REST vs GraphQL vs tRPC vs gRPC 2026年完全ガイド: APIアーキテクチャの正しい選び方
新しいプロジェクトを始めようと空のファイルを開いた瞬間、議論が始まります。
「RESTでいいんじゃない?みんな知ってるし」誰かがそう言うと、チームメンバーが「前職でGraphQL使ってたけど最高だった」と口を挟み、もう一人が「tRPCが未来でしょ」とつぶやき、シニアバックエンドエンジニアが腕を組んで「マイクロサービスならgRPC一択」と主張します。
この議論、あらゆるチームのあらゆる新プロジェクトで繰り返されています。2026年になっても答えは**「場合による」**ですが、今はその判断材料がかなり揃っています。
このガイドでは、REST、GraphQL、tRPC、gRPCが2026年のプロダクションで実際にどう動いているかを比較します。2020年のチュートリアルではなく、今の基準で。アーキテクチャ、パフォーマンス、開発者体験、そして誰も語らないコストの現実まで全部カバーして、最後には迷わず選べる判断基準をお伝えします。
状況が変わりました
これらの技術へのイメージが2022年で止まっているなら、現実とズレた前提で考えていることになります:
2022年以降の変化:
REST:
→ OpenAPI 3.1が事実上の標準に(JSON Schemaと整合)
→ Fetch APIがどこでも使える(Node, Deno, Bun, ブラウザ)
→ HTMXがRESTをフロントエンドの議論に引き戻した
GraphQL:
→ Federation v2が成熟(Apollo, Grafbase, WunderGraph)
→ Relay CompilerがReact Server Componentsと統合
→ Subscriptionsはまだ微妙;大半のチームはSSEを使用
tRPC:
→ v11リリース: React Server Componentsネイティブ対応
→ TanStack Start + tRPCが新しいフルスタックの定番に
→ TypeScript専用のまま(それがポイント)
gRPC:
→ gRPC-Webが安定化; Connectプロトコルの採用が拡大
→ Buf.build + ConnectRPCがDXを劇的に改善
→ Protocol Buffers → TypeScriptコード生成が楽に
大事なのは:万能な選択肢はないということ。それぞれ最適化の方向が違います。よくある失敗は、要件ではなくハイプで選んでしまうことです。
30秒おさらい:それぞれ何者か
比較に入る前に、基本をさっと確認しておきましょう:
REST
Client: GET /api/users/123
Server: { "id": 123, "name": "Alice", "email": "[email protected]" }
Client: GET /api/users/123/orders?limit=5
Server: [{ "id": 1, "product": "Widget", "total": 29.99 }, ...]
リソース指向。URL1つにリソース1つ。HTTPメソッド(GET、POST、PUT、DELETE)で操作を定義し、サーバーが返すデータを決めます。
GraphQL
query { user(id: 123) { name email orders(limit: 5) { product total } } }
HTTP上のクエリ言語。エンドポイントは1つ(/graphql)。クライアントが取得するデータを選び、サーバーが型システムを通じてフィールドを解決します。
tRPC
// Server(ルーター定義) export const appRouter = router({ user: router({ getById: publicProcedure .input(z.object({ id: z.number() })) .query(async ({ input }) => { return db.users.findUnique({ where: { id: input.id } }); }), }), }); // Client(直接関数呼び出し — コード生成なし、fetchなし) const user = await trpc.user.getById.query({ id: 123 }); // ^? { id: number, name: string, email: string }
TypeScript推論によるEnd-to-endの型安全性。スキーマ定義言語なし。コード生成なし。ルーターそのものがAPIの契約です。
gRPC
// user.proto service UserService { rpc GetUser (GetUserRequest) returns (User); rpc ListOrders (ListOrdersRequest) returns (stream Order); } message User { int32 id = 1; string name = 2; string email = 3; }
HTTP/2上のバイナリプロトコル(Protocol Buffers)。スキーマを先に定義してコードを生成する方式で、ストリーミングがネイティブで使えます。サービス間通信のために作られたプロトコルです。
本当の比較:実際に何が重要か
パフォーマンス
誰も見せてくれないもの — 同じ操作(ユーザー+注文5件取得)での実測レイテンシとペイロードサイズ:
プロトコル ペイロード(bytes) シリアライズ レイテンシ(p50) レイテンシ(p99)
────────────── ─────────────── ────────── ───────────── ─────────────
REST (JSON) 1,247 ~0.3ms 12ms 45ms
GraphQL 834 ~0.5ms 15ms 55ms
tRPC (JSON) 1,180 ~0.2ms 11ms 40ms
gRPC (proto) 312 ~0.1ms 4ms 12ms
補足:
- RESTは~30%不要なフィールドも取得(over-fetching)
- GraphQLにはリゾルバーのオーバーヘッドあり
- tRPCは生RESTとほぼ同等のオーバーヘッド
- gRPCはワイヤーサイズで圧勝だがHTTP/2が必要
- すべてNode.js 22、同一マシン、同一DBで計測
ポイント:ブラウザ→サーバー通信では、REST、GraphQL、tRPC間のパフォーマンス差はほぼ無視できるレベルです。結局ネットワークレイテンシが支配的なので。gRPCが真価を発揮するのは、両端をコントロールできて毎秒何千回も呼び出すサービス間通信のみです。
型安全性
本当の差が出るのはここです:
プロトコル スキーマソース クライアント型 ランタイム検証
─────────── ────────────────── ────────────── ───────────────
REST OpenAPI (任意) コード生成必要 手動
GraphQL SDL (必須) コード生成必要 スキーマ検証
tRPC TypeScript自体 自動(推論) Zod内蔵
gRPC Protobuf (必須) コード生成必要 Proto検証
// REST: 型を自分で書く(合ってることを祈る) const res = await fetch('/api/users/123'); const user = await res.json() as User; // 🤷 信じてくれ // GraphQL: スキーマからコード生成(ビルドステップ1つ追加) const { data } = useQuery(GET_USER); // コード生成が走れば型あり // tRPC: 型が自動的に流れる(追加作業ゼロ) const user = await trpc.user.getById.query({ id: 123 }); // ^? サーバーのZodスキーマ+戻り値型から推論 // gRPC: .protoからコード生成(ビルドステップ1つ追加) const user = await client.getUser({ id: 123 }); // protoから型生成
tRPCの最大の強み:サーバーでフィールド名を変更 → クライアントのコードに赤い波線が即座に出ます。ビルドステップなし。コード生成なし。「型を再生成したっけ?」という不安もなし。
tRPCの最大の弱み:クライアントとサーバーが両方ともTypeScriptで、同じリポジトリ(またはパッケージ共有)にある場合にしか動きません。
開発者体験
それぞれで毎日コードを書いたらどんな感じか、率直にまとめます:
REST:
✅ みんな知ってる(学習コストゼロ)
✅ Curlフレンドリー(デバッグしやすい)
✅ 膨大なツールエコシステム
❌ 自動型安全性なし
❌ Over-fetching / under-fetchingがデフォルト
❌ バージョニングが面倒(v1, v2, v3...)
❌ 複雑なUIでN+1エンドポイント問題
GraphQL:
✅ クライアント主導のクエリ(UIに必要なものだけ取得)
✅ 自己文書化するスキーマ
✅ 複雑でネストされたデータに強い
❌ キャッシングが難しい(HTTPキャッシュが使えない)
❌ リゾルバーレベルでN+1クエリ問題
❌ Mutationが取って付けた感じ
❌ フルスタックの学習曲線が急
❌ ファイルアップロードがつらい
tRPC:
✅ ゼロオーバーヘッドの型安全性
✅ 覚えるスキーマ言語なし
✅ モノレポDXが最高
✅ Mutationが自然に書ける
❌ TypeScript専用(両端とも)
❌ パブリックAPIには不向き
❌ クライアント・サーバー間の密結合
❌ REST/GraphQLより小さいエコシステム
gRPC:
✅ 最高の生パフォーマンス
✅ ネイティブストリーミング(双方向)
✅ 優れた後方互換性
✅ 多言語コード生成
❌ ブラウザネイティブではない(プロキシ/Connect必要)
❌ Protobufという別言語を覚える必要
❌ デバッグがつらい(バイナリプロトコル)
❌ 学習曲線が急
キャッシング
キャッシングに関しては、RESTが圧倒的に有利です:
REST:
HTTPキャッシングがそのまま動く™
- CDNキャッシング(Cache-Controlヘッダー)
- ブラウザキャッシング(ETag、条件付きリクエスト)
- プロキシキャッシング(Varnish, Nginx)
- URL1つ = 一意なキャッシュキー
GraphQL:
HTTPキャッシングが実質壊れている
- 単一エンドポイントへのPOST = URLベースのキャッシュ不可
- GETベースのキャッシュにはPersisted Queriesが必要
- 専用キャッシュレイヤーが必要(Apollo, Stellate)
- キャッシュ無効化が複雑(正規化キャッシュ)
tRPC:
HTTPキャッシングが動作する(クエリはGET)
- TanStack Queryがクライアントキャッシングを処理
- 適切なヘッダーでCDNキャッシュ可能
- キャッシュキー = プロシージャパス + input
gRPC:
HTTPキャッシングなし(バイナリプロトコル)
- カスタムキャッシュインフラが必要
- 通常はサービスメッシュレベルで解決(Envoy, Istio)
- リクエストメッセージのハッシュでキャッシュ
CDNキャッシュが効果的なAPI(公開データ、更新頻度の低いリソース)なら、RESTに勝つのは難しいです。
N+1問題:みんな抱えていて、みんな解き方が違う
N+1問題は、どのAPIスタイルでも一度はハマるパフォーマンスの落とし穴です。それぞれの対処法を見ていきましょう:
REST N+1
クライアントが必要とするもの:
- ユーザープロフィール
- ユーザーの最新注文10件
- 各注文の配送状況
RESTアプローチ(ナイーブ):
GET /api/users/123 → 1リクエスト
GET /api/users/123/orders → 1リクエスト
GET /api/orders/1/shipping → 1リクエスト
GET /api/orders/2/shipping → 1リクエスト
... (あと10回) → 10リクエスト
合計: 12 HTTPリクエスト 😱
RESTアプローチ(スマート):
GET /api/users/123?include=orders.shipping → 1リクエスト
(またはデータを集約するBFFエンドポイント)
GraphQL N+1
# クライアントはリクエスト1つ!(いいね!) query { user(id: 123) { name orders(last: 10) { id shipping { status, eta } # ← ここでリゾルバーレベルのN+1が発生 } } }
// サーバー側の問題: const resolvers = { Order: { shipping: (order) => db.shipping.findByOrderId(order.id) // 10回呼ばれる!注文ごとに1回! } } // 解決策: DataLoader const shippingLoader = new DataLoader( (orderIds) => db.shipping.findByOrderIds(orderIds) ); const resolvers = { Order: { shipping: (order) => shippingLoader.load(order.id) // 1つのクエリにバッチ処理 } }
tRPC N+1
// tRPCはデフォルトでこの問題がありません // 1つのprocedureでクエリ全体をコントロールするので: const userWithOrders = await trpc.user.getWithOrders.query({ id: 123 }); // サーバー側: JOINまたはバッチロードで1クエリ // データフェッチロジックを自分で書くので、クエリも自分でコントロール
gRPC N+1
// gRPCはサービス境界で解決: rpc GetUserWithOrders(GetUserRequest) returns (UserWithOrders); // またはストリーミングを使用: rpc StreamOrderUpdates(OrderRequest) returns (stream OrderUpdate);
まとめ: GraphQLはN+1をクライアントからサーバーに移します。RESTはクライアントに委ねます。tRPCとgRPCは、目的に合ったprocedure/RPCを定義できるので根本的に回避できます。
現実のアーキテクチャパターン
パターン1: フルスタックTypeScriptアプリ(tRPC)
適合: SaaSアプリ、ダッシュボード、社内ツール
┌──────────────────────────────────────┐
│ Next.js / TanStack Start フロントエンド│
│ (React + TanStack Query) │
│ │ │
│ tRPC Client │
│ │ (型推論) │
│ ▼ │
│ tRPC Server (Zodバリデーション) │
│ │ │
│ Database (Prisma / Drizzle) │
└──────────────────────────────────────┘
なぜうまくいくか:
- DBカラムを変更 → UI層で即座に型エラー
- APIドキュメント不要(TypeScriptがドキュメント)
- Zodが入力を検証、Prismaが出力を検証
- 1つのリポ、1つの言語、1つの型システム
パターン2: パブリックAPIプラットフォーム(REST + OpenAPI)
適合: 開発者プラットフォーム、公開API、マルチクライアントアプリ
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Webクライアント│ │ モバイルアプリ│ │ サードパーティ│
└─────┬──────┘ └──────┬─────┘ └──────┬─────┘
│ │ │
└────────────┬────┘─────────────────┘
▼
┌──────────────┐
│ REST API │
│ (OpenAPI 3.1)│
│ + Swagger │
└──────┬───────┘
│
┌──────▼───────┐
│ Services │
└──────────────┘
なぜうまくいくか:
- どの言語/プラットフォームからでも利用可能
- OpenAPIが全言語用SDKを生成
- HTTPキャッシュ + CDN = 無料のスケーリング
- RESTは誰でも理解できる
パターン3: データの多いダッシュボード(GraphQL)
適合: 分析ダッシュボード、CMS、マルチエンティティ管理画面
┌────────────────────────────────────────┐
│ 管理ダッシュボード (React) │
│ │
│ ┌─────────┐ ┌──────────┐ ┌────────┐│
│ │ ユーザー │ │ 分析 │ │コンテンツ││
│ │ パネル │ │ チャート │ │エディタ ││
│ └────┬────┘ └────┬─────┘ └───┬────┘│
│ │ │ │ │
│ └─────── GraphQL ─────────┘ │
│ (ビューごとに1クエリ) │
└───────────────────┬────────────────────┘
▼
┌───────────────┐
│ GraphQLサーバー│
│ (Federation) │
├───────────────┤
│ Usersサービス │
│ Analytics DB │
│ CMSサービス │
└───────────────┘
なぜうまくいくか:
- 各パネルが必要なデータだけ正確に取得
- ビューごとに1リクエスト(ウォーターフォールなし)
- Federationでチームごとにスキーマを管理
- スキーマ = 自動ドキュメント
パターン4: マイクロサービスバックエンド(gRPC)
適合: 高スループットバックエンド、ポリグロットサービス、リアルタイムシステム
┌──────────────┐
│ API Gateway │ (外部にはREST/GraphQL)
└──────┬───────┘
│ gRPC (内部)
▼
┌──────────────┐ ┌──────────────┐
│ User Service │◄───►│ Order Service│
│ (Go) │ │ (Rust) │
└──────┬───────┘ └──────┬───────┘
│ │
│ gRPC │ gRPC
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Auth Service │ │ Payment Svc │
│ (Python) │ │ (Java) │
└──────────────┘ └──────────────┘
なぜうまくいくか:
- バイナリプロトコル = 帯域幅5〜10倍削減
- リアルタイム更新のためのストリーミング
- Protoスキーマ = 言語間の契約
- サービスメッシュがディスカバリ+ロードバランシング
ハイブリッドが現実です
「REST vs GraphQL」ブログ記事で誰も言わない真実:ほとんどのプロダクションシステムは複数を併用しています。
2026年の典型的なSaaSアーキテクチャ:
外部:
┌─────────────────┐
│ パブリックREST API│ (インテグレーション、Webhook、SDK用)
└────────┬────────┘
│
内部:
┌────────▼────────┐
│ tRPC / GraphQL │ (自社フロントエンド用)
└────────┬────────┘
│
バックエンド:
┌────────▼────────┐
│ gRPC / REST │ (サービス間通信)
└─────────────────┘
これはオーバーエンジニアリングではありません — 使う人が違えば、最適なツールも違うというだけです:
- 外部開発者には安定性とドキュメントが重要 → REST + OpenAPI
- 自社フロントエンドにはDXと型安全性が最優先 → tRPC(クライアントが複数ならGraphQL)
- サービス間ではパフォーマンスとスキーマ進化が鍵 → gRPC(シンプルならREST)
どう選ぶか
議論はここで終わりにしましょう。このフローチャートに従ってください:
START: 誰がこのAPIを使うか?
├── 外部開発者 / パブリックAPI
│ └── REST + OpenAPI 3.1
│ (汎用的、キャッシュ可能、広く理解されている)
│
├── 自社フロントエンド(TypeScriptモノレポ)
│ ├── データ要件がシンプル?
│ │ └── tRPC
│ │ (ゼロオーバーヘッド、最大の型安全性)
│ └── 複雑なネストデータ / 複数クライアント?
│ └── GraphQL
│ (柔軟なクエリ、クライアント主導)
│
├── サービス間通信(内部マイクロサービス)
│ ├── ストリーミング / 高スループットが必要?
│ │ └── gRPC
│ │ (バイナリプロトコル、ネイティブストリーミング)
│ └── 少数のサービス間で単純なCRUD?
│ └── REST
│ (シンプルに)
│
└── よくわからない / プロトタイプ段階?
└── RESTで始める
(後からいつでも移行できる)
「それを選んではいけない」シナリオ
時には何を選ばないかを知ることが最良のアドバイスです:
❌ GraphQLを使うべきではない場合:
- データがシンプルでフラット(CRUDアプリ)
- HTTPキャッシングを積極的に使いたい
- チームにGraphQL経験がまったくない
- フロントエンド1つで予測可能なデータ要件
❌ tRPCを使うべきではない場合:
- クライアントがTypeScriptではない
- パブリックAPIが必要
- C/Sが別リポジトリで別のデプロイサイクル
- モバイルアプリも同じAPIを使う
❌ gRPCを使うべきではない場合:
- ブラウザクライアントしかない(動くが辛い)
- サービス5個未満(オーバーキル)
- チームがProtocol Buffersを学びたがらない
- デバッグ時にワイヤーフォーマットを人間が読む必要がある
❌ RESTを使うべきではない場合:
- フロントエンドが深くネストされた可変データを必要とする
- TypeScriptモノレポアプリ(tRPCの方が厳密に優れている)
- 双方向リアルタイムストリーミングが必要
移行パス:一度選んだら終わりではない
一番怖いのは、間違った選択をして身動きが取れなくなること。でも安心してください。移行パスはすでに確立されていて、多くのチームが実際に歩んでいます:
REST → GraphQL
// 既存のRESTエンドポイントをGraphQLリゾルバーでラップ const resolvers = { Query: { user: async (_, { id }) => { const res = await fetch(`${REST_BASE}/users/${id}`); return res.json(); }, orders: async (_, { userId }) => { const res = await fetch(`${REST_BASE}/users/${userId}/orders`); return res.json(); }, }, }; // 段階的にリゾルバーをDB直接アクセスに移行 // クライアント移行: クエリを1つずつ
REST → tRPC
// tRPCはRESTと同じサーバーで共存可能 import { createExpressMiddleware } from '@trpc/server/adapters/express'; const app = express(); // 既存のRESTルートはそのまま動作 app.get('/api/v1/users/:id', existingHandler); // 新しいtRPCルーターを並列にマウント app.use('/trpc', createExpressMiddleware({ router: appRouter })); // エンドポイントを1つずつ移行
GraphQL → tRPC
// TypeScriptモノレポなら移行は簡単です: // 1. GraphQLクエリに対応するtRPC procedureを定義 // 2. コンポーネントを1つずつ移行 // 3. 使われなくなったGraphQLリゾルバーを削除 // Before (GraphQL): const { data } = useQuery(gql` query GetUser($id: ID!) { user(id: $id) { name, email } } `); // After (tRPC): const { data } = trpc.user.getById.useQuery({ id }); // 同じ結果、コード生成なし、即座の型フィードバック
コスト分析:隠れたコスト
開発時間以上に、各プロトコルにはインフラコストの差があります:
インフラコスト比較(大規模: 1日1,000万リクエスト):
REST GraphQL tRPC gRPC
────────────────── ────────── ────────── ────────── ──────────
CDNキャッシュ 非常に良い 悪い 良い N/A
帯域幅 基準 -20〜30% ~基準 -60〜80%
サーバーCPU 基準 +20〜40% ~基準 -10〜20%
ツーリングコスト 無料 $$ 無料 $
モニタリング 標準 専門ツール 標準 専門ツール
ゲートウェイ 標準 GraphQL GW 標準 gRPCプロキシ
隠れたコスト:
REST: APIバージョン管理のメンテナンス
GraphQL: クエリ複雑度分析、クエリコストベースのレートリミティング
tRPC: TypeScript依存以外なし
gRPC: Proto管理、サービスメッシュ
GraphQLの隠れたコスト: 規模が大きくなると、クエリ複雑度分析、persisted queries、depth limiting、専用APMツールが必要になります。このインフラ税は無視できないもので、後になって気づくチームが多いです。
gRPCの隠れた帯域幅削減: サービス間トラフィックが最大のコストなら(マイクロサービスでは一般的)、gRPCのバイナリエンコーディングで帯域幅を60〜80%削減できます。
2026年の結論
手っ取り早く答えが欲しい方へ:
| シナリオ | 最善の選択 | 次善 |
|---|---|---|
| パブリックAPI | REST + OpenAPI | GraphQL |
| TypeScriptモノレポSaaS | tRPC | REST |
| マルチプラットフォーム(Web+モバイル+サードパーティ) | GraphQL | REST |
| マイクロサービス(内部) | gRPC | REST |
| シンプルなCRUDアプリ | REST | tRPC |
| リアルタイム双方向データ | gRPC | GraphQL (subscriptions) |
| データ量の多い管理画面 | GraphQL | tRPC |
| プロトタイピング / MVP | REST | tRPC |
一番大事なのは:これは宗教ではないということです。2026年の優秀なチームは、レイヤーごとに違うプロトコルを使い分けています。パブリックAPIはREST、自社フロントエンドはtRPC、バックエンドのマイクロサービスはgRPC。これらはツールであって、アイデンティティではありません。
どのプロトコルが「客観的に優れている」かの議論はもう終わりにしましょう。代わりにこう聞いてみてください:「誰がこのAPIを使い、その人たちの制約は何で、自分のチームはいま何を知っているのか?」
比較表ではなく — その問いが、答えを教えてくれます。
関連ツールを見る
Pockitの無料開発者ツールを試してみましょう