APIの方式を決める場面でgRPCとGraphQLが並ぶと、比較表を見ても決め手が出てきません。バイナリ対JSON、HTTP/2対HTTP/1.1という対比は事実ですが、選定理由にはならないためです。両者は同じ問題を別の方法で解いているのではなく、解いている問題そのものが違います。gRPCはサーバ同士の呼び出しを速く固く行うため、GraphQLは画面が欲しいデータをクライアント側から指定するために作られました。それぞれの成り立ちはgRPCの仕組みとRESTとの違いとGraphQLの定義とメリット・デメリットに譲り、ここでは判断に効く差だけを扱います。
まとめ|先に決める3項目と併用構成に寄せる境界
結論を先に置きます。この2つは択一で悩む対象ではなく、置く層が違う道具です。判断の入口は次の3項目だけで足ります。
- 呼び出し元は誰か:ブラウザやモバイルの画面が直接叩くならGraphQL、サーバやバッチが叩くならgRPC。ブラウザから素のgRPCは呼べません
- 取得形が固定か可変か:画面ごとに欲しいフィールドが変わるならGraphQL、呼ぶ処理が決まっているならgRPC
- 契約を何人で共有するか:多言語のチームが両側を生成物で揃えたいならgRPC、フロント側の開発速度を上げたいならGraphQL
規模が上がると、択一ではなく外向きにGraphQL、内部呼び出しにgRPCという併用へ寄ります。画面の都合はゲートウェイで吸収し、サービス間は型の固い呼び出しで通す形です。寄せる境界と寄せずに済ませる条件は、後段で言い切ります。
| 観点 | gRPC | GraphQL |
|---|---|---|
| 主な想定 | サーバ間のRPC | クライアント駆動の取得 |
| 公開年と提供元 | 2016年・Google | 2015年・Meta |
| 契約の書き方 | .protoのIDL | SDLのスキーマ |
| 転送形式 | Protobufのバイナリ | おおむねJSON |
| 下位の通信 | HTTP/2が前提 | HTTP/1.1でも可 |
| 生成物の範囲 | 両側のスタブを生成 | 主にクライアント型 |
| エンドポイント | サービスとメソッド単位 | 単一のURLに集約 |
| ブラウザ直呼び | 不可(中継が必要) | 可(普通のPOST) |
gRPCとGraphQLの違い|生まれた背景が別で担う層が重ならない
両者を同じ棚に並べると混乱します。順に成り立ちを見ると、比較すべき軸が自然に絞れます。
gRPC|HTTP/2とProtobufでメソッドを呼び出す仕組み
gRPCは、離れた場所にある関数を呼ぶ形でサービス間をつなぐフレームワークです。インタフェースは.protoに書き、送受信はProtocol Buffersのバイナリになります。下位はHTTP/2で、1本の接続に複数のストリームを流せるため、呼び出しごとに接続を張り直す負荷がありません。呼び出しの形は単一往復(unary)、サーバ側ストリーミング、クライアント側ストリーミング、双方向ストリーミングの4種が公式のコアコンセプトに定義されています。実装系列は言語ごとに分かれ、Goのgrpc-goは1.83系が現行です(2026年9月時点・GitHubのリリース一覧で確認)。
設計の重心は「呼ぶ処理が事前に決まっている」ことにあり、そのうえで転送効率と型の固さを取りにいく道具だと捉えると外しません。
GraphQL|単一エンドポイントへ問い合わせるクエリ言語とランタイム
GraphQLはクエリ言語とサーバ側ランタイムの組で、呼び出し側が欲しいフィールドを書いて送ると、その形のままJSONが返ります。エンドポイントは1つに集約され、URLの設計で表現力を稼ぐ必要がありません。仕様は版が明示され、直近の版はSeptember2025です(仕様リポジトリのタグとEditionコミットで確認)。前の版がOctober2021だったため、4年ぶりの版更新にあたります。
重心は「取得したい形が呼び出し側の都合で変わる」ことです。画面が増えるたびにエンドポイントを足していた作業を、スキーマの範囲内でクライアント側へ移します。
どちらが速いかは条件で入れ替わる|測っている区間がそれぞれ違う
速度比較は、何を測るかで結論が入れ替わります。gRPCが効くのは1回の呼び出しの直列化と転送量。GraphQLが効くのは往復回数で、画面表示に要る3回の取得を1回に畳めば遅延の合計が縮みます。前者はサービス間の高頻度呼び出し、後者はモバイル回線での画面初期化で差が出るという関係です。同じ土俵に並べた数字だけで決めると判断を誤ります。
スキーマ定義とコード生成の比較|契約をどこに置くかで保守が変わる
どちらもスキーマ駆動を名乗りますが、契約の置き場所と生成物の範囲が異なり、長く保守するほどこの差が効きます。
protoとprotocの生成物|サーバとクライアントを同時に作る
gRPCの契約は.protoファイルです。サービスとメソッド、メッセージのフィールドとその番号を書きます。
syntax = "proto3";
package catalog.v1;
service CatalogService {
rpc GetProduct(GetProductRequest) returns (Product);
rpc ListProducts(ListProductsRequest) returns (stream Product);
}
message GetProductRequest {
string product_id = 1;
google.protobuf.FieldMask read_mask = 2;
}
message Product {
string product_id = 1;
string name = 2;
int64 price_yen = 3;
reserved 4;
reserved "legacy_code";
}
この1ファイルからprotocがサーバ側の抽象クラスとクライアント側のスタブを生成します。Goなら次の形です。
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
catalog/v1/catalog.proto
生成物には引数と戻り値の型、メソッドのシグネチャ、直列化の処理まで含まれます。呼ぶ側は生成されたメソッドを呼ぶだけで、URLもHTTPメソッドも書きません。同じ.protoを共有すればJavaもPythonもTypeScriptも同じ契約から型を持てて、契約が破られればコンパイル時点で落ちます。
SDLとGraphQL Code Generator|型はクライアント側へ寄る
GraphQLの契約はSDLで書いたスキーマです。型と、その型が持つフィールド、入口となるQueryとMutationを定義します。
type Product {
id: ID!
name: String!
priceYen: Int!
reviews(first: Int = 10): [Review!]!
}
type Review {
id: ID!
score: Int!
body: String!
}
type Query {
product(id: ID!): Product
products(keyword: String, first: Int = 20): [Product!]!
}
ここから生成できるのは、主にクライアント側の型と、クエリごとの結果型です。GraphQL Code Generatorによる型生成を使えばTypeScriptの型やReactのフックまで出せます。一方でサーバ側は、フィールドごとの解決処理(リゾルバ)を人が書く前提が残ります。スキーマは書けても中身の取得処理は生成されないため、gRPCのように「両側が生成物で埋まる」形にはなりません。ここが作業量の見積もりを外しやすい箇所です。
スキーマの破壊的変更への耐性|番号予約と非推奨マークの運用手順
フィールドを消すときの作法も違います。Protobufはフィールド番号が転送上の識別子なので、削除した番号を別の意味で再利用すると古いクライアントがデータを誤って解釈します。公式の言語ガイドが示す運用は、削除した番号と名前をreservedで予約すること。先ほどの.protoの2行がそれです。
GraphQLは名前で解決するため番号の衝突はありませんが、フィールドを消せばそれを含むクエリが即座に失敗します。運用では@deprecatedを付けて残し、使われなくなったことを計測してから消す手順を踏みます。誰がどのフィールドを使うかが単一エンドポイントで見えるのは利点で、サーバ側で利用を記録すれば削除の判断材料が手元に残るはずです。
| 比較軸 | gRPC | GraphQL |
|---|---|---|
| 契約ファイル | .protoのIDL | SDLのスキーマ |
| 生成の範囲 | サーバ雛形と両側の型 | 主に呼び出し側の型 |
| 人が書く部分 | メソッドの中身 | リゾルバの中身 |
| 削除時の作法 | 番号と名前を予約 | 非推奨にして計測 |
| 破綻の見え方 | ビルドで落ちる | 実行時にエラー |
データ取得の効率を比較|オーバーフェッチとN+1が起きる層の違い
「GraphQLはオーバーフェッチを解消する」という説明はよく見ます。ただし解消した分の負担が消えるわけではなく、置き場所が移るだけです。移った先で何が起きるかを押さえておかないと、公開後に応答が伸びます。
GraphQLが解くオーバーフェッチと、代わりに背負うN+1問題
RESTで商品一覧とレビューを出すと、一覧APIと商品ごとのレビューAPIを呼ぶことになり、往復も余分なフィールドも増えます。GraphQLなら1回のクエリで必要なフィールドだけを取れます。
query ProductList {
products(keyword: "sensor", first: 20) {
id
name
reviews(first: 3) { score }
}
}
クライアント側から見れば往復は1回です。しかしサーバ側では20件の商品に対してreviewsのリゾルバが20回呼ばれ、素朴に実装すれば一覧の1クエリに加えてレビュー取得が20クエリ走ります。これがN+1で、クライアントから見えない場所で起きるため開発中の少件数では気づけません。gRPCなら「一覧とレビューをまとめて返すメソッド」を人が設計するので、その中でJOINやIN句に落とす判断が自然に入ります。
DataLoaderでのバッチ化とクエリ複雑度の制限を実装する
GraphQLでN+1を抑える定石は、同一イベントループ内の呼び出しをまとめるバッチ処理です。Node.jsならDataLoaderを挟みます。
const reviewLoader = new DataLoader(async (productIds) => {
const rows = await db.query(
"SELECT * FROM reviews WHERE product_id = ANY($1)",
[productIds]
);
const byProduct = new Map(productIds.map((id) => [id, []]));
for (const r of rows) byProduct.get(r.product_id).push(r);
return productIds.map((id) => byProduct.get(id));
});
const resolvers = {
Product: {
reviews: (parent) => reviewLoader.load(parent.id),
},
};
20回のロードが1回の問い合わせに畳まれます。あわせて要るのが、クエリの重さに上限を設ける仕組みです。GraphQLは呼び出し側が形を決められる以上、深い入れ子や大量件数の要求も書けてしまいます。深さの上限、フィールド重み付けによる複雑度の上限、永続化クエリによる受付範囲の限定を公開前に決めておくこと。外部公開のAPIでこれを省くと、利用側の書き方ひとつでデータベースが詰まります。
gRPCはメソッド単位|取りすぎはFieldMaskで絞る設計にする
gRPCは戻り値のメッセージが固定なので、呼ぶ側が「名前だけ欲しい」と言えません。返す項目を増やすほど、全員が全部を受け取ります。この対処が標準型のFieldMaskです。取得したいフィールドをパス列で渡し、サーバ側がその集合だけを詰めて返します。
req := &catalogv1.GetProductRequest{
ProductId: "p-1024",
ReadMask: &fieldmaskpb.FieldMask{Paths: []string{"name", "price_yen"}},
}
res, err := client.GetProduct(ctx, req)
ただしFieldMaskはサーバ側で解釈して実装する必要があり、GraphQLのように仕組みとして常に効くものではありません。判断としては、フィールドの取捨がクライアントごとに大きく変わるならGraphQL側の性質が要り、変わらないならgRPCで固定して構いません。取得形の可変性が要件として実在するかが分かれ目で、将来必要になるかもしれないという理由でGraphQLを敷くと、複雑度制限とN+1対策の実装だけが増えます。
経路と到達性を比較|ブラウザ・プロキシ・CDNキャッシュで通る条件
方式の性能が出るかどうかは、両端だけでなく間に挟まる装置で決まります。採用後に作り直しへ至りやすいのがこの部分です。
ブラウザから素のgRPCを呼べない制約とgRPC-Webでの回避
ブラウザのfetchやXHRからは、HTTP/2のフレームを直接組み立てられません。画面からgRPCのサービスを直接呼ぶことはできず、Envoyなどのプロキシを置いてgRPC-Web形式へ変換する中継が要ります。しかもgRPC-Webで使えるのは単一往復とサーバ側ストリーミングまでで、クライアント側と双方向のストリーミングは対象外です。詳細はブラウザからgRPCを呼ぶ構成にまとめています。
GraphQLはこの制約がありません。単一URLへのPOSTなので、fetchで送れます。画面が直接叩くAPIを新設するという要件なら、この一点だけでGraphQL側に寄ります。
GraphQLはPOST一本|CDNキャッシュが効きにくい理由と対処
逆に、単一URLへのPOSTであることは運用面の制約にもなります。CDNやリバースプロキシの多くはURLとメソッドでキャッシュを判断するため、本文が違っても同じURLへのPOSTでは既定のキャッシュが働きません。同じ一覧を1万人が見るような画面では、この差が配信コストに直結します。
対処は3つあります。1つ目はGETと永続化クエリの組み合わせで、クエリをハッシュ化してIDで参照し、GETで送ってURL単位のキャッシュに乗せる方法です。2つ目はクエリと変数の組をキーにしたサーバ側の応答キャッシュ。3つ目は割り切りで、よく読まれる導線だけをRESTやCDNの静的配信へ分離します。gRPCもHTTPキャッシュとは相性が悪く、キャッシュ層はサービス側の実装に寄ります。読み取りが多く同じ内容が繰り返されるなら、方式選定より配信経路の設計が先です。
監視とレート制限で生じる差|メソッド単位と単一エンドポイントの比較
gRPCはサービス名とメソッド名が経路に現れるため、既存の監視やアクセスログがそのまま呼び出し単位の分解能を持ちます。レート制限も、重いメソッドだけを絞る形で経路側の装置に任せられます。GraphQLは全部が同じURLに来るので、経路の装置からは1種類の通信にしか見えません。操作名の記録、複雑度に応じたコスト計算、利用者単位の予算といった仕組みを、サーバ側かゲートウェイ側に自分で持つ必要があります。
BFFでGraphQL、内部をgRPCにする併用構成の設計と実装
ここまで見た性質は排他ではありません。画面に近い側の要求(形が可変・ブラウザから直接・往復を減らす)と、サービス間の要求(型が固い・速い・多言語)は、層を分ければ両方満たせます。
併用構成の形|外向きはGraphQL、内部の呼び出しはgRPC
形は単純です。ブラウザとモバイルはBFFのGraphQLエンドポイントだけを見ます。BFFはリゾルバの中で商品・在庫・レビューの各サービスをgRPCで呼び、結果を組み立てて返す役です。スキーマは画面の都合、内部の.protoはサービスの都合で設計し、両者を無理に一致させないのが要点になります。
この形ならフロントはエンドポイントの増設を待たずに画面を作れ、バックエンドは内部インタフェースを型で固められます。BFFのスキーマが肥大したらサブグラフを合成するフェデレーション構成へ進む道も残ります。
リゾルバからgRPCを呼び出す実装|期限の伝播とエラー対応付け
実装で外しやすいのは、境界をまたぐ2つの情報です。期限とエラーの意味を、GraphQL側とgRPC側で対応付けます。
const resolvers = {
Query: {
product: async (_parent, { id }, ctx) => {
const deadline = new Date(Date.now() + ctx.remainingMs);
return new Promise((resolve, reject) => {
ctx.catalog.GetProduct({ product_id: id }, { deadline }, (err, res) => {
if (!err) return resolve(res);
if (err.code === grpc.status.NOT_FOUND) return resolve(null);
if (err.code === grpc.status.PERMISSION_DENIED) {
return reject(new GraphQLError("forbidden", {
extensions: { code: "FORBIDDEN" },
}));
}
reject(new GraphQLError("upstream unavailable", {
extensions: { code: "UPSTREAM_ERROR" },
}));
});
});
},
},
};
期限は、BFFに与えられた残り時間から逆算して内部呼び出しへ渡します。渡さないと、画面側がとうに諦めた処理を内部で走らせ続けることになります。エラーはgRPCのステータスコードをそのまま外へ出さず、意味の対応表を作って変換するのが基本です。NOT_FOUNDをnullで返すかエラーで返すかはスキーマの設計判断。部分的な失敗の扱いも決めておきます。GraphQLは一部のフィールドだけエラーにして残りを返せるため、在庫サービスだけが落ちているときに商品名は表示する設計も可能です。
併用構成を見送る場面|層が一段増えたときに支払う運用コストの内訳
併用は無料ではありません。支払うものは、BFFのデプロイと監視、スキーマとprotoの二重管理、障害時に切り分ける対象がひとつ増えること、そしてBFFがすべての読み取りの単一障害点になることです。サービスが2〜3個で、画面も1種類しかないなら、この対価に見合いません。素直にgRPCだけ、RESTだけで足ります。
目安を条件で示します。内部サービスが5個以上あり、画面がWebとモバイルの2系統以上あり、フロントとバックエンドの担当が分かれている。この3つが揃うときに併用が効きます。1つでも欠けるなら単層で始め、必要になってからBFFを足す順序が安全です。
使い分けの判断基準|採用する3条件と見送る場面を条件付きで示す
最後に、迷わず決めるための条件を並べます。2つ以上当てはまる側を選べば大きく外しません。
gRPCを選ぶ3条件|通信相手と型契約と多言語の状況から決める
- 呼び出し元がサーバかバッチ:ブラウザが直接叩かないなら、中継プロキシを置く理由がなくなり、gRPCの不利が消えます
- 呼ぶ処理が事前に決まっている:メソッドと引数が固定なら、可変クエリの仕組みは要りません。決まった処理を速く固く通す形が合います
- 言語が混在している:GoとPythonとJavaが混じる構成では、1つの.protoから全言語の型を生成できる利点が保守の実額に直結します
加えて、大きなデータを流し続ける要件(ログ転送、機器のテレメトリ、逐次生成される結果の配信)があるならgRPCが向きます。ストリーミングが4方式そろい、1接続の上を複数のやり取りが並走できるためです。受信メッセージ長は既定で4MBなので、大きな塊を1回で送るなら上限の引き上げか分割を先に決めます。
GraphQLを選ぶ3条件|画面主導の要求と組織の分かれ方で決める
- 画面ごとに欲しいデータが違う:一覧・詳細・ダッシュボードで必要なフィールドが毎回変わるなら、可変クエリの価値が出ます
- フロントの改修速度を上げたい:エンドポイント追加の依頼と待ちが開発の律速になっているなら、スキーマの範囲でフロントが自走できます
- クライアントが複数系統ある:Web・iOS・Androidで必要な形が違っても、1つのスキーマで賄えます
ただし採用と同時に、複雑度の上限・永続化クエリ・N+1対策・フィールド単位の権限確認が実装対象に入ります。ここを後回しにした構成は、公開後の負荷か情報漏えいのどちらかで問題になりがちです。RESTから移る場合の判断材料はRESTとGraphQLの使い分けと運用コストにまとめてあります。
どちらも選ばない境界|RESTとtRPCへ寄せたほうがよい条件
3つ目の選択肢を早めに検討したほうがよい場面もあります。公開APIとして不特定多数に配る、あるいは利用側の技術力に幅があるなら、RESTのほうが通りが良くCDNキャッシュもHTTPの作法だけで効きます。サーバもクライアントもTypeScriptで同じリポジトリに入っているなら、コード生成の工程なしで型を共有できるtRPCが軽く収まるはずです。tRPCとgRPC・GraphQLの違いで、型安全を保ちながら道具を減らす選択肢を比べています。
言い切ると、不特定多数に配るならREST、同一言語の閉じた構成ならtRPC、サーバ間の性能と多言語ならgRPC、画面主導で形が変わるならGraphQLです。この4分割で足りないときだけ併用構成を検討します。
受託開発で契約前に合意する3項目|スキーマと期限と互換性の方針
外部の開発会社と進めるときは、着手前に3つを文書で決めます。1つ目はスキーマの所有者と変更手続き。.protoやSDLをどのリポジトリで管理し、変更に誰の承認が要るかを決めないと、後半で結合が止まります。2つ目は期限とリトライの方針。呼び出し全体の制限時間、内部呼び出しへの残り時間の配分、再実行してよい操作の範囲を先に決めます。3つ目は互換性の保証範囲で、削除可能なフィールドの条件と非推奨から削除までの期間を定めます。
この3項目は納品後の保守費に直接効きます。設計から実装、公開後の運用まで含めて相談したい場合は、API開発・システム連携で受けている支援内容が判断の材料です。接続方式が決まっていない段階からでも、要件を分解して方式を選ぶところから並走できます。
よくある質問
gRPCとGraphQLはどちらが速いですか?
測る区間で入れ替わります。1回の呼び出しの直列化と転送量ではgRPCが有利で、画面表示までの往復回数ではGraphQLが有利です。方式そのものより、N+1の有無とキャッシュの効き方のほうが実測値への影響は大きくなります。
GraphQLでストリーミングのような継続配信はできますか?
できます。仕様の範囲ではサブスクリプションが継続配信の入口で、実装はWebSocket上のgraphql-wsが一般的です。実装とスケールの考え方はGraphQLサブスクリプションの実装で扱っています。多数の接続を保持する運用設計は別途要るため、一方向配信で足りるならgRPCのサーバ側ストリーミングのほうが構成は単純です。
社内のサービス間通信でGraphQLを使ってはいけませんか?
禁止する理由はありません。ただし呼び出し元がサーバなら、必要なフィールドはコードに書かれた時点で確定します。可変クエリの利点が働かない一方で複雑度の制限やN+1対策は要るため、この条件では推奨しません。
既存のREST APIからどちらへ移行すべきですか?
全面移行より、境界を切って足す進め方を勧めます。画面の取得回数が多いことが課題ならその導線だけをGraphQLのBFFで包み、サービス間の遅延と型崩れが課題なら内部呼び出しだけをgRPCへ置き換えます。RESTは残したまま並走させ、実測で効果を確認してから範囲を広げる順序が安全です。
スキーマ管理を.protoかSDLのどちらかに寄せられますか?
片方から他方を自動生成する道具はありますが、生成結果は画面の都合を反映しません。.protoはサービスの都合、SDLは画面の都合で設計するものなので、機械的に一致させると両方が使いにくくなります。併用構成では二重管理を受け入れ、対応表とテストで整合を守る運用が現実的です。
関連記事
- gRPCとは?仕組み・RESTとの違い・UDP/TCPの扱いを実装例で解説:gRPC側の基礎とトランスポートの扱い
- GraphQLとは?REST APIとの違い・メリット・デメリットをわかりやすく解説:GraphQL側の基礎と得失
- REST APIとGraphQLの違いと使い分け|選定基準と運用コスト:対比相手をRESTにした場合の判断
- tRPCとは?REST・GraphQL・gRPCとの違いと型安全なAPI実装:同一言語構成での第3の選択肢
- gRPC-Webとは?ブラウザからgRPCを呼ぶ仕組みとプロキシ構成・採用判断を解説:ブラウザ到達性の詳細
- GraphQLフェデレーションとは?サブグラフ合成とルーターの実装・採用判断を解説:BFFが肥大した後の分割
- GraphQL Code Generatorとは:SDLからクライアント型を生成する工程
- GraphQLサブスクリプションとは?graphql-wsでの実装とスケール設計を解説:継続配信が要るときの実装