RPC(型安全クライアント)
サーバ側のルート定義から型を抜き出して、クライアントをfetchのラッパとして自動生成する仕組み。
tRPC のような体験を、普通の REST のまま得られるのが Hono RPC の強み。
仕組み
- サーバ側の
appの型をexportする - クライアント側でその型を
hc<typeof app>(url)に渡す - クライアントがサーバのルートに合った API オブジェクトとして使える
- レスポンスは
typeof res.json()も型推論される
サーバ側
server/api.ts
import { Hono } from "hono"
import { z } from "zod"
import { zValidator } from "@hono/zod-validator"
const app = new Hono()
.get("/users/:id", (c) => {
return c.json({ id: c.req.param("id"), name: "Alice" })
})
.post(
"/users",
zValidator("json", z.object({ name: z.string(), email: z.string() })),
(c) => {
const data = c.req.valid("json")
return c.json({ id: "1", ...data }, 201)
},
)
.get(
"/search",
zValidator("query", z.object({ q: z.string() })),
(c) => {
const { q } = c.req.valid("query")
return c.json({ q, results: [] })
},
)
export default app
export type AppType = typeof app // ★これが大事
クライアント側
client/api.ts
import { hc } from "hono/client"
import type { AppType } from "../server/api"
export const client = hc<AppType>("https://api.example.com")
使用
// GET /users/:id
const res = await client.users[":id"].$get({ param: { id: "1" } })
const user = await res.json() // { id: string, name: string }
// POST /users
const res2 = await client.users.$post({
json: { name: "Alice", email: "a@example.com" },
})
const created = await res2.json()
// GET /search?q=hello
const res3 = await client.search.$get({ query: { q: "hello" } })
const data = await res3.json()
IDE でパスもパラメータも候補に出る。サーバを変更してクライアントの型が壊れたらコンパイルエラー。
メソッド名
$get/$post/$put/$delete/$patch- 型のためのプロパティで、実際は内部で fetch を呼ぶ
引数
| キー | 用途 |
|---|---|
param | パスパラメータ |
query | クエリ |
json | JSON ボディ |
form | フォームボディ |
cookie | Cookie |
header | ヘッダー |
追加オプション
await client.users.$post(
{ json: { name: "A", email: "a@b.c" } },
{
headers: { Authorization: "Bearer ..." },
init: { signal: abortController.signal },
},
)
ステータスコード別の型
Hono RPC はステータスごとに型を分けない(res.json() は OK 時の型)。
エラーは例外的な型ガードで扱う:
const res = await client.users[":id"].$get({ param: { id: "1" } })
if (!res.ok) {
const err = await res.text()
throw new Error(err)
}
const data = await res.json()
URL の生成だけしたい
// $url() で URL を取り出せる
const url = client.users[":id"].$url({ param: { id: "1" } })
// → URL("https://api.example.com/users/1")
fetch の差し替え
グローバル fetch をカスタマイズした fetchに置き換えたい時:
const client = hc<AppType>("https://api.example.com", {
fetch: (url, init) => {
return fetch(url, {
...init,
credentials: "include", // Cookie を送る
headers: { ...init?.headers, "x-app": "web" },
})
},
})
共通ヘッダーを入れる
const client = hc<AppType>("https://api.example.com", {
headers: () => ({
Authorization: `Bearer ${getToken()}`,
}),
})
nest(サブアプリの型維持)
サーバが app.route("/api", api) のように分割されているなら、
クライアントもその構造で:
// server
const api = new Hono().get("/users", ...)
const app = new Hono().route("/api", api)
export type AppType = typeof app
// client
const client = hc<AppType>("https://example.com")
client.api.users.$get()
ファイルをアップロード
const fd = new FormData()
fd.append("file", file)
fd.append("title", "My Photo")
await client.upload.$post({ form: fd as any })
tRPC との比較
| Hono RPC | tRPC | |
|---|---|---|
| プロトコル | 普通の REST(fetch) | 独自 |
| クライアント | fetch ラッパ | 専用クライアント |
| 外部から叩ける | ○(普通の HTTP) | △(HTTP 互換) |
| 型安全 | ○ | ○ |
| サブスクリプション | SSE / WebSocket は別途 | 組み込み |
| エコシステム | 普通の Web | 専用 |
REST の API として外部にも公開したいならHono RPC、 モノリポ内で完結し型統合の恩恵を最大化したいならtRPC。
注意点
- サーバ側でチェーンを切らない(
const app = new Hono(); app.get(...); app.get(...)のようにバラバラに書くと型推論が崩れる)。必ず.get(...)をチェーン。 - 同じプロジェクト内(モノリポ)か 型を package として共有する仕組みが必要
- サーバの動的処理が複雑だと型がギブアップする場合あり。明示的に
satisfiesやキャストで補強 - クライアントは fetch ベースなのでCORS 等は普通通り必要
活用パターン
✓ BFF + フロントのモノリポ
✓ Cloudflare Workers の API + Astro / Next.js のフロント
✓ React Native アプリのバックエンド(型を npm package で共有)
✓ 内部 admin ツール