TL;DR
- tRPC は「同じチームが TypeScript で書いた frontend と backend」を最短距離で繋ぐ道具で、RPC の再発明ではない。
- 採用判断は「クライアント言語が TS だけか」「外部公開が必要か」「何年動かすか」の 3軸 で決める。最初の主要2問で 多くのケースが決まる(経験則)。
- 採用後の負債は バージョニング・観測性・キャッシュ に集中する。設計時に決めておけば、撤退コストも下がる。
はじめに
こんにちは、みねです。
「tRPC は型安全で楽だから、もう REST も GraphQL も要らない」という意見を社内・コミュニティでよく見かけます。半分は正しく、半分は危険です。tRPC が解いている問題は、REST や GraphQL が解いている問題と違います。同じ尺度で比べると、必ずどこかで地雷を踏みます。
この記事のゴール: tRPC を採用すべきかをチームで議論できる材料を、判断軸 / 比較表 / 運用負債 / 撤退条件まで含めて整理することです。前提として、tRPC は v11 系(2026-05時点)を想定しています。v10 → v11 では TanStack Query v5 連携、transformer 配置、HTTP adapter / Content-Type、proxy 名などに破壊的変更が入っているため、古い記事のサンプルをそのまま貼ると周辺機能で動かない箇所があります(initTRPC.create() / t.router() / t.procedure.input().query() の基本 API は v10 と互換)。
この記事がカバーしないこと:
- tRPC の入門チュートリアル(公式ドキュメントが最良)
- 個別フレームワーク(Next.js / Express / Hono)への組み込み手順
- BFF と組み合わせた多段アーキテクチャの詳細
1. tRPC の本質 — 型を契約として共有する仕組み
tRPC を一言で言うと、「サーバ側で書いた関数の型を、クライアント側がそのまま import して使える仕組み」です。
REST は HTTP メソッドと URL を契約とし、GraphQL は SDL(Schema Definition Language)を契約とします。tRPC は TypeScript の型そのもの を契約にしています。だから「スキーマを書かなくていい」と言われますが、実体は スキーマの代わりに型を共有しているだけで、契約が消えたわけではありません。
procedure と router
tRPC の最小単位は procedure(手続き)です。これは「入力 → 出力」を持つ1つの関数で、query(読み取り)/ mutation(書き込み)/ subscription(リアルタイム購読)の3種類があります。本記事の最小例では query と mutation のみ扱いますが、WebSocket 経由の購読も procedure として表現できます。procedure を集めたものが router で、router をネストすると API のツリーができあがります。
雰囲気を掴むため、最小例を1つだけ示します。
// server/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
user: t.router({
getById: t.procedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return { id: input.id, name: 'taro' };
}),
}),
});
export type AppRouter = typeof appRouter;
クライアントは AppRouter を 型としてのみ import type で取り込み ます(実装ロジックは含みません)。型推論は Awaited<ReturnType<...>> を辿って効くので、IDE上で client.user.getById.query({ id: '1' }) の戻り値が補完されます。
注: 上記は サーバ側 router の最小例です。クライアントを動かすには別途
createTRPCClient<AppRouter>({ links: [httpBatchLink({ url: '/api/trpc' })] })のようなクライアント生成と HTTP adapter / link 設定が必要です(公式ドキュメント 参照)。
「スキーマレス」の正体は型推論
AppRouter の型は、TypeScript の conditional types と inference を駆使して、procedure ツリー全体をクライアント用の型に変換しています。仕組み自体は TypeScript Handbook の型操作 を読み込めば理解できますが、要点は「コードジェネレータが要らない」ことです。
GraphQL と比較するとここが顕著です。GraphQL は SDL からクライアントの型を生成するため、ビルドステップに graphql-codegen などが入ります。tRPC は型を直接 import するため、ビルドステップが1つ減ります。これが「短く書ける」と言われる主因です。
Zod でランタイム境界を守る
型は コンパイル時 にしか効きません。HTTP 経由でクライアントから来た入力が型通りである保証は無いので、tRPC では Zod などのランタイムバリデータで input() を守ります。ここを省くと「型は通っているのに NaN が DB に入る」ような事故が起きます。Zod スキーマは TypeScript 型を自動的に推論できるため、二重定義にはなりません。
tRPC 公式ドキュメント でも、input での Zod 利用が標準パターンとして紹介されています。
2. REST / GraphQL / tRPC を比較する
比較表(採用判断の軸で並べる)
| 観点 | REST + OpenAPI | GraphQL | tRPC |
|---|---|---|---|
| 契約の表現 | URL + JSON Schema | SDL | TypeScript 型 |
| クライアント言語 | 任意(多言語) | 任意(多言語) | TypeScript のみ |
| コード生成 | OpenAPI Generator | graphql-codegen | 不要 |
| バージョニング | URL / ヘッダ | スキーマ非互換警告 | 標準なし(自前設計) |
| キャッシュ | HTTP 標準 | 個別実装 | 個別実装 |
| 公開API 適合性 | 高い | 高い | 低い |
| 学習コスト(TSチーム) | 中 | 中〜高 | 低 |
tRPC が「短く書ける」のは理由がある
tRPC のコードが短いのは、スキーマを別ファイルに書かないからではなく、スキーマと型を一体化させているからです。これは「GraphQL より優れている」ではなく、「異なる前提で書いている」と理解するのが正確です。クライアントが TypeScript 1択、というのが前提です。
別言語のクライアント(Swift / Kotlin / Python)が必要になった瞬間、tRPC の優位性は消えます。逆に、チーム全員が TS を書いていて、API も TS で書くような環境では、tRPC は単純に手数を減らします。
GraphQL が解く問題と tRPC が解く問題は違う
GraphQL 公式 の Why GraphQL を読むと、設計動機が明確に書かれています。GraphQL は「クライアントが必要なフィールドだけを取得する」ためのクエリ言語で、Over-fetching / Under-fetching の解消が一番の目的です。tRPC はこの問題を解いていません。procedure ごとに返すフィールドは固定です。
逆に、tRPC が解いているのは「型を二度書く問題」です。GraphQL でも codegen でほぼ解決できますが、ビルドステップが残ります。問題の輪郭が違うので、「どちらが優れているか」ではなく「どちらの問題が自分のチームで深刻か」で選ぶべきです。
REST + OpenAPI は両者の中間で、「多言語クライアント + 契約の永続化」を最重視する場合に最も無難な選択肢です。
3. 採用判断の3軸 — 言語統一 / 公開要件 / 寿命
ここまでで道具の特性は揃ったので、判断軸に落とします。3つの軸で評価して、採用条件を満たすかを見ます。
軸1: クライアント言語が TS に統一されているか
- 統一されている → tRPC 候補
- 1つでも別言語クライアントが想定される → REST / GraphQL を選ぶ
「将来 iOS / Android アプリを出すかも」という不確実性がある場合は、今は出さない なら tRPC でも構いません。ただし、移行コストを ADR に書いておくこと。
軸2: 外部公開・パートナーAPI として使うか
- 内部利用のみ(同じチームの frontend だけが叩く)→ tRPC 候補
- 外部公開・他社が叩く・SDK を配る → REST / GraphQL
外部公開する API は、契約の永続化 が要件になります。OpenAPI / SDL は人間が読める仕様書として配布できますが、tRPC の AppRouter 型は TS の型情報として import type する形式で、人間が読める言語非依存の配布フォーマットを公式には持ちません(trpc-openapi で OpenAPI 出力する手段は存在しますが、2024-11 に archived・v10 前提のため v11 では選定注意)。
軸3: 何年動かすか(寿命)
- 1〜2 年の MVP → tRPC でも問題が顕在化しにくい
- 5 年以上動かす基幹システム → 慎重に。バージョニング戦略を先に作る
tRPC はバージョニングの標準が無いので、長寿命で破壊的変更が頻発するシステムでは、設計コストが高くなります。
主要2問で大半が決まるチェックリスト(経験則)
実務では、上の3軸のうち最初の2つに答えるだけで決断できることが多いです。
- クライアントは TypeScript だけか?(YES / NO)
- 外部公開する予定はあるか?(YES / NO)
| Q1 | Q2 | 推奨 |
|---|---|---|
| YES | NO | tRPC(最有力) |
| YES | YES | REST + OpenAPI(または GraphQL) |
| NO | NO | REST または GraphQL |
| NO | YES | REST + OpenAPI |
深掘り記事: 技術選定で後悔しないための判断基準
4. 採用後の運用負債を先に潰す
採用判断のあと、運用フェーズで詰みやすいポイントが3つあります。これは tRPC 固有の弱点というより、「設計を先送りしたら詰む」だけの話ですが、tRPC は手早く始められるぶん、設計を先送りしやすい性質があります。
バージョニング戦略
tRPC には「v1, v2 で別 endpoint」のような標準仕様がありません。実運用での選択肢は次の3つです。
- router 階層で分ける:
appRouter.v1.user.getByIdのように、router をバージョンごとにネスト - procedure 名で分ける:
getUserByIdV2のように suffix - deprecate アノテーションで案内: 入力スキーマを段階的に拡張し、古い形は警告ログ
内部利用のみなら 3 が現実的です。外部に類するクライアント(複数チーム)が叩く場合は 1 を最初から採用する設計が安定します。
観測性(OpenTelemetry / ログ)
tRPC は middleware で procedure 単位の計測ができますが、HTTP レイヤから見ると「全部 /trpc に POST」になりがちで、URL ベースの監視ダッシュボードが破綻します。最初から procedure 名を span / log にタグ付け する middleware を入れること。
エラー応答の形式も自前設計が必要で、外部APIに比較すると標準化が弱いです。エラー設計の詳細は エラーハンドリング設計の指針 で扱っています。
キャッシュ戦略(HTTP キャッシュとの相性)
REST が当然のように使える HTTP キャッシュ(Cache-Control / ETag / CDN)は、tRPC では工夫しないと使えません。query を GET に変換するオプションはありますが、入力サイズによっては URL に乗り切らず POST に倒れます。
エッジで読み取りを高速化したいケースでは、query だけ別エンドポイントで HTTP GET に倒し、CDN キャッシュを効かせる工夫が必要です。エッジランタイム前提なら Hono と組み合わせる構成が普及しています。
5. 段階導入と撤退条件
「tRPC をいきなり全面採用」ではなく、段階導入 が安全です。
tRPC + REST 併用パターン
- 内部向け(同一モノレポの frontend が叩く)→ tRPC
- 外部向け(パートナー / SDK / モバイルアプリ)→ REST
- 同じドメインモデルを共有層に置き、両方から呼ぶ
これなら、外部公開要件が後から増えても、REST 側を独立に成長させられます。境界をどこに引くかは、責務分離の議論と同じ構造です。設計の骨格は 責務分離が崩壊する理由と設計原則 を参考にしてください。
撤退条件を ADR に書く
採用時に、撤退条件を ADR に明記します。例:
## Status
Accepted - 2026-05-01
## Context
内部 frontend と backend が両方 TS。スキーマ二重定義のコストが高い。
## Decision
tRPC v11 を社内向け API として採用する。
## Consequences
- Pros: 型のずれによる本番事故が減る見込み
- Cons: 多言語クライアント要件が増えると再設計が必要
- 撤退条件: 6ヶ月後にモバイルアプリ要件が確定したら REST 側に移管。procedure 数 50 を超える前に評価する。
撤退条件を 数値 で決めるのが要点です(参考: 技術選定で後悔しないための判断基準)。「うまくいかなければ撤退」だけでは、誰も引き金を引きません。
FAQ
Q1. tRPC のデメリットは何ですか?
主に3つです。多言語クライアントに対応できない、バージョニング標準がない、HTTP キャッシュが素直に効かない。いずれも内部API用途では問題になりにくく、外部公開・長期運用で顕在化します。
Q2. tRPC はモノレポが前提ですか?
技術的には別リポジトリでも AppRouter の型さえ共有できれば動きますが、型を共有する仕組みを別途作る必要があります。モノレポなら型を直接 import できるので運用が楽です。pnpm workspace / Turborepo などとの相性は良好です。
Q3. tRPC から OpenAPI を出力できますか?
trpc-openapi を使うと procedure に HTTP メソッド / パスを割り当てて OpenAPI 文書を生成できます。ただし 2024-11-19 に該当リポジトリは archived され、README 上も tRPC v10 前提です。v11 想定の記事として推奨手段にはせず、現行で OpenAPI が必要な場合は本体側の代替(@trpc/openapi の動向)か、最初から「外部公開層は REST」と決めて分けるほうが境界が明確です。HTTP メソッド/パスを割り当てる構造から REST 風 API にはなりますが、tRPC 内部の型推論は維持されます。
Q4. Next.js との相性は?
App Router 以降は Server Actions が tRPC と機能的に重なる部分があります。Server Actions だけで足りる場合は tRPC を入れる必要はありません。tRPC を選ぶ理由は「Server Actions では届かない型推論の細かさ」「procedure を整理する router の階層」「フロントが Next.js 以外のクライアント(Expo / Vite + React など)にも広がる可能性」のいずれかが必要なときです。
Q5. すでに動いている REST API から移行する価値はありますか?
「動いていて困っていない」なら移行コストに見合いません。移行を検討する基準は 型ずれによる本番事故の頻度(一般的な経験則)と、スキーマ二重定義の保守工数 です。両方とも高い場合のみ、段階移行を検討します。
tRPC を採用したあとのデータ取得層キャッシュ戦略は別軸の設計判断になる。Next.js を使っているなら Next.js Cache Components 移行設計 で扱う 'use cache' directive と cacheTag での invalidation 設計が、tRPC procedure の手前にもう一段キャッシュ境界を引く道具になる。
まとめ
tRPC は「TS で書く frontend と backend を最短距離で繋ぐ道具」です。汎用 API フレームワークとして REST や GraphQL の代替になるわけではありません。採用判断は クライアント言語が TS か / 外部公開するか / 何年動かすか の3軸、特に主要2問で多くのケースが決まる傾向にあります(経験則)。
採用したあとは、バージョニング・観測性・キャッシュの3点を設計時に決めておくこと。撤退条件は数値で ADR に書くこと。これらに加えて、複数チーム / 別言語クライアント / 公開契約 / 移行コストといった長期運用要因にも目を配れば、tRPC は十分に長期運用の選択肢となり得ます(経験則)。
採用判断や ADR の整備で迷ったら、相談ください。一緒に整理します。
- 次に読む: 技術選定で後悔しないための判断基準
- 次に読む: エラーハンドリング設計の指針
- 次に読む: 責務分離が崩壊する理由と設計原則
