Hono RPC + Superjson で厳密に型を共有する
はじめに
今回はHonoのRPCを利用する際に、フロント・バックエンドで厳密に型を共有する方法を紹介します。
SuperJSONで型情報をjsonに追加した上で、zod.parse()を利用して値を検証して型を付与します。
利用技術
- Hono v4.7.11: Webフレームワーク
- SuperJSON v2.2.2: jsonに型情報を付与する
- Zod v3.25.56: バリデーション
- TypeScript
背景
HonoのRPC機能では、フロントエンドから下記のようにAPIを呼び出すことができます。
// バックエンド側のレスポンス作成部分 export const getSampleHandler = async (c) => { return c.json({ message: "test", date: new Date(), }); }; // フロントエンド側のAPIコール部分 const response = await client.api.sample.$get(); // 'GETメソッドで /api/sample を呼び出す' const data = await response.json();
この時dataの型情報を見ると、dateがstring型になっています。

これはレスポンスに設定したobjectがjsonに変換される際にjsonで扱える形式にキャストされるため発生します。 せっかくRPCを使うのであれば、すべての型情報を共有したいのでSuperJSONを活用することにしました。
関連情報
下記の記事で、今回やろうとしていることが解説されていましたが、現在のHonoのバージョンではそのまま利用することができませんでした。
またGitHubのissueでも同じことが議論されていましたが、具体的なコードは提示されないままクローズされています。 github.com
同issueの中でzod.parse()について言及があったため、これを利用して実装してみました。
github.com
実装
バックエンド側
SuperJSONを使ってレスポンスが返せるように、下記の関数を準備します。
export const jsonS = <T>( c: Context, object: T, status: StatusCode = 200, headers?: Record<string, string> ) => { const body = SuperJSON.stringify(object); const responseHeaders = { "content-type": "application/json; charset=UTF-8", "x-SuperJSON": "true", // SuperJSON形式でシリアライズしていることを表現 ...headers, }; return c.newResponse(body, status, responseHeaders); };
ヘッダーを追加するので、CORSの設定を追加します。
const app = new Hono() .use( "*", cors({ ・・・ exposeHeaders: ["x-SuperJSON"], // 許可する ・・・ }) )
上記の関数を利用してAPIのレスポンスを作成します。
export const getSampleHandler = async (c: Context) => { const data = { message: "test", date: new Date(), }; return jsonS(c, data); };
合わせてレスポンスのスキーマをZodで定義します。
export const sampleResponseSchema = z.object({ message: z.string().describe("サンプルメッセージ"), date: z.date().describe("日付"), });
バックエンド側の実装はこれで終了です。
フロントエンド
こちらもヘルパー関数を用意して、SuperJSONとZodを使ってobjectを取得します。
export async function parseTypedResponse<T extends z.ZodSchema>( response: Response, schema: T ): Promise<z.infer<T>> { const text = await response.text(); // SuperJSONパース let parsedData: any; if (response.headers.get("x-SuperJSON")) { parsedData = SuperJSON.parse(text); } else { parsedData = JSON.parse(text); } // Zodバリデーション return schema.parse(parsedData); }
APIのコール部分は下記のイメージです。
const response = await client.api.sample.$get(); const data = await parseTypedResponse(response, sampleResponseSchema);
これでdate型の型情報が付与されました。

おわりに
今回はHono RPC + SuperJSON を使ってDate型も含めた型共有を実現してみました。 Zodのスキーマ情報をフロント・バックエンドで共有する必要が出てくるので、もう少しスマートに実現できると嬉しい気がします。