Hono の RPC Client の仕組み
はじめに
この記事は Hono Advent Calendar 2024 の 20 日目の記事です。
今年は「Hono」のソースコードをすべて理解するというチャレンジを個人的にしてました。
Hono をご存じない方もいるかも知れないので簡単にご紹介すると、Hono は超軽量で高速なウェブフレームワークで、Node.js 環境でも Deno 環境でも、そしてブラウザでも動作するモダンな設計が特徴です。リポジトリはこちらから。
ちなみに、最近 Wikipedia も出来ました。
https://ja.wikipedia.org/wiki/Hono
遂に先日、全てを読み終えたので、その中でも特にブラックボックスであろう Hono の RPC Client の仕組みについてでも簡単に書こうと思います。
もし良ければ、いいね・共有お願いします!
Hono の RPC Client とは?
Hono の RPC Client とは hc のことです。
(RPCというのはここではサーバーとクライアント側でリクエスト/レスポンスの型を共有することを指します。)
ドキュメント: https://hono.dev/docs/guides/rpc
以下のように使用できます。
- サーバー側
// backend.ts
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const app = new Hono()
const route = app.post(
'/posts',
zValidator(
'form',
z.object({
title: z.string(),
body: z.string(),
})
),
(c) => {
// ...
return c.json(
{
ok: true,
message: 'Created!',
},
201
)
}
)
export type AppType = typeof route
後々分かりますが、この部分がとても重要です。
export type AppType = typeof route
- クライアント側
// frontend.ts
import { AppType } from './backend.ts'
import { hc } from 'hono/client'
const client = hc<AppType>('http://localhost:8787/')
const res = await client.posts.$post({
form: {
title: 'Hello',
body: 'Hono is a cool project',
},
})


ちゃんと型が付いているのが分かります。
これはどのように実現されているんでしょうか?
探索
先ほど述べた通り、この部分が非常に重要になってきます。
export type AppType = typeof route
この型はどうなっているのでしょうか?
ホバーして覗いてみます。

何やらAppTypeの中に、リクエストやレスポンスの型が含まれています。

この部分には、リクエストのパスとメソッドの情報が含まれています。
パス: /posts
メソッド: $post

そしてこの部分にはバリデータによって定義されたリクエストの型の情報が含まれています。

そしてこれはハンドラーのレスポンスの型の情報が含まれています。
ここで、なぜ typeof app とせず、 typeof route としているのかというと、TypeScript の制限が関係しています。
TypeScript では、途中で呼ばれた関数によって変更された型や narrowing (型の絞り込み) を追うことが出来ません。
その為、Hono では Generics (ジェネリクス) にルートの情報を積んでいくことで、リクエストやレスポンスの型を保存できます。
以下は Handler の型定義です。
ここには Input と HandlerResponse という二つの型があります。
見ての通り、リクエストの型やレスポンスの型を Generics に保持しています。
これらはバリデータや、c.json 等により生成され、Handler の型を経由して Generics に積まれます。
これらを 先ほどの AppType のスキーマの形にして Generics に積んでいきます。
これらがクライアント側で組み立てられることで実現されます。
具体的には、
この部分で Client の型が組み立てられています。
重要なのはこの部分
名前から想像が付くかもしれませんが、
/api/posts/list => client.api.posts.list のようにパスをプロパティのチェーンで指定できる機能の型を生成しています。

TypeScript Playground
そして末端のパスに ClientRequest という型が生えます。
client.api.posts.list なら list の部分に生えます。
この型には $url や $get, $post などのリクエストを送る関数が生えます。
この関数の型には、バリデータや c.json 等で生成されたリクエスト、レスポンスの型が渡され、型安全に通信を行えるという訳です。
内部的には、 JavaScript の Proxy という機能を用いて実装されています。
ここで、$post => POST のように変換され、
ここでheadersやbodyが生成されます。
そして、ここでリクエストが飛ぶというわけです。
そして、$get や $post のレスポンスには、 ClientResponse という型が割り当てられています。
見ての通り、通常の Response 型が上手くラップされています。 Response.ok は、予想される status が成功であるか、失敗であるかを自動的に場合分けし、型を予想します。
Response.status は予想されるステータスコードが割り当てられます。
他はそのままです。
ここが特に重要です。
ここでレスポンスの Body の型を推測しています。
このように、上手く型が組み合わせられ、Hono の RPC Client は実現されています。
最後に
良いお年を!
app.get('/next-year', async (c, next) => {
await next()
return c.text('Happy new year 2025!')
})
Discussion