RPC(型安全クライアント)

サーバ側のルート定義から型を抜き出して、クライアントをfetchのラッパとして自動生成する仕組み。 tRPC のような体験を、普通の REST のまま得られるのが Hono RPC の強み。

仕組み

  1. サーバ側の appexport する
  2. クライアント側でその型を hc<typeof app>(url) に渡す
  3. クライアントがサーバのルートに合った API オブジェクトとして使える
  4. レスポンスは 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 でパスもパラメータも候補に出る。サーバを変更してクライアントの型が壊れたらコンパイルエラー

メソッド名

引数

キー用途
paramパスパラメータ
queryクエリ
jsonJSON ボディ
formフォームボディ
cookieCookie
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 RPCtRPC
プロトコル普通の REST(fetch)独自
クライアントfetch ラッパ専用クライアント
外部から叩ける○(普通の HTTP)△(HTTP 互換)
型安全
サブスクリプションSSE / WebSocket は別途組み込み
エコシステム普通の Web専用

REST の API として外部にも公開したいならHono RPC、 モノリポ内で完結し型統合の恩恵を最大化したいならtRPC

注意点

活用パターン

BFF + フロントのモノリポ
✓ Cloudflare Workers の API + Astro / Next.js のフロント
✓ React Native アプリのバックエンド(型を npm package で共有)
✓ 内部 admin ツール