2026.01.06TypeScriptAPI

Hono と tRPC を組み合わせた理由

SpecPilot のバックエンドを書き始めたとき、フレームワーク選定で少しだけ迷った。最終的に Hono を下に敷いて、tRPC を上に載せて、OpenAPI は Lint 用のドキュメントとして横に置く という構成にした。少し変則的なので理由を残しておく。

下は Hono

シンプルに「軽くて、どこでも動く」が大きい。Cloud Run / Vercel / Cloudflare Workers のどれに乗せても動く。middleware の API も読みやすく、書き足すのも楽。

SpecPilot は将来 iOS クライアントや CLI(vibe-pack を吐くコマンド)から叩く可能性があって、その時に 「HTTP のフレームワークとして枯れていること」 が効いてくる。tRPC オンリーで組んでしまうと、後で外から HTTP で叩きたい場面に必ず詰まる。だから HTTP の口は Hono、型の通信は tRPC で役割を分けた。

上に tRPC

同じ Next.js アプリの中からは tRPC で叩く。「クライアントとサーバーの型が一致しないと作業が止まる」からで、これは個人開発で何より優先したい体験。

const app = new Hono();

app.use('/trpc/*', trpcServer({
  router: appRouter,
  createContext,
}));

tRPC ルータは独立 module で、Hono に「載っているだけ」。後で外部に API を開きたくなったら Hono の通常ルートを足せばいい。 tRPC をやめずに、tRPC の外を増やせる 構造になっている。

OpenAPI は Lint 専用

ここがやや変則的。SpecPilot の OpenAPI は 「契約として運用する」のではなく「Lint の参照ドキュメント」として置いている

tRPC で内部の型同期はもう十分やれている。OpenAPI を Source of Truth にしてもうれしさが薄い。一方で、意思決定ログ(D-XXXX)や設計書から「ここに来れば API 仕様が読める」場所はあってほしい。なので OpenAPI は読める形で残す・Lint で齟齬を検出する用途に絞った。SDK 自動生成にもフロント実装の Single Source 化にも使っていない。


最初は「tRPC か OpenAPI か」と二者択一で悩んでいたけど、 両方やる、ただし役割を分ける が今のところ一番しっくり来ている。tRPC が前線、OpenAPI が後方ドキュメント。