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 の型定義です。
見ての通り、リクエストの型やレスポンスの型を Generics に保持しています。
これらはバリデータや、c.json
等により生成され、Handler の型を経由して Generics に積まれます。
これらを 先ほどの AppType のスキーマの形にして Generics に積んでいきます。
これらがクライアント側で組み立てられることで実現されます。
具体的には、
重要なのはこの部分
名前から想像が付くかもしれませんが、
/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