Back

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メソッド(GETPOSTPUTDELETE)で操作を定義し、サーバーが返すデータを決めます。

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年の結論

手っ取り早く答えが欲しい方へ:

シナリオ最善の選択次善
パブリックAPIREST + OpenAPIGraphQL
TypeScriptモノレポSaaStRPCREST
マルチプラットフォーム(Web+モバイル+サードパーティ)GraphQLREST
マイクロサービス(内部)gRPCREST
シンプルなCRUDアプリRESTtRPC
リアルタイム双方向データgRPCGraphQL (subscriptions)
データ量の多い管理画面GraphQLtRPC
プロトタイピング / MVPRESTtRPC

一番大事なのは:これは宗教ではないということです。2026年の優秀なチームは、レイヤーごとに違うプロトコルを使い分けています。パブリックAPIはREST、自社フロントエンドはtRPC、バックエンドのマイクロサービスはgRPC。これらはツールであって、アイデンティティではありません。

どのプロトコルが「客観的に優れている」かの議論はもう終わりにしましょう。代わりにこう聞いてみてください:「誰がこのAPIを使い、その人たちの制約は何で、自分のチームはいま何を知っているのか?」

比較表ではなく — その問いが、答えを教えてくれます。

RESTGraphQLtRPCgRPCAPI DesignSystem DesignTypeScriptNode.jsArchitectureWeb Development

関連ツールを見る

Pockitの無料開発者ツールを試してみましょう