先に要点
記事一覧にタイトルと著者名を出したい。スマホ版ではタイトルだけ、管理画面では著者の情報も詳しく欲しい。このように、同じデータから画面ごとに違う項目を使うとき、APIの返し方が設計上の問題になります。
GraphQLとREST APIを、この一つの画面を通して比べます。取得の柔軟さだけでなく、その裏で必要になるデータ取得・認可・エラー処理まで見ると、採用する理由と見送る理由を整理できます。
GraphQLとRESTは、何を決める仕組みなのか
GraphQLはAPIの問い合わせ言語と実行の仕組みです。サーバーは「記事にはタイトルと著者がある」という型を定義し、クライアントはその中から欲しい項目を指定します。この型の定義をスキーマと呼びます。
RESTは、リソースを扱うインターフェースやステートレスな通信などを組み合わせた設計様式です。この記事では、URLで記事や利用者を表し、GETなどのHTTPメソッドで操作する一般的なHTTP APIを比較対象にします。単にURLが複数あることだけがRESTの定義ではありません。
どちらでも、DBから取り出す処理や、他のサービスへ問い合わせる処理はサーバー側で実装します。GraphQLを選んでも、SQLや業務ロジックが不要になるわけではありません。
同じ記事一覧をREST APIで取得する
まず「記事と著者を別々に返すAPI」がある場合を考えます。下のURLと応答は比較のための設計例です。
GET /articles?limit=2
[
{"id":"1","title":"Article 1","authorId":"u1"},
{"id":"2","title":"Article 2","authorId":"u2"}
]
これだけでは著者名がないので、クライアントはGET /authors/u1とGET /authors/u2で名前を取りに行きます。この設計なら一覧1回と著者2回です。画面の表示に足りないデータを追加取得する問題をアンダーフェッチと呼びます。
ただし、RESTでは必ず分割しなければならないわけではありません。たとえばGET /articles?limit=2&include=authorで著者も含めるAPIを設計できます。
[
{"title":"Article 1","author":{"name":"Author A"}},
{"title":"Article 2","author":{"name":"Author B"}}
]
この場合は1回のHTTPリクエストで画面に必要な情報が揃います。includeはこの例で決めたパラメータで、REST共通の機能ではありません。取得項目を選ぶパラメータや、画面向けの集約APIを用意することも可能です。
同じ一覧をGraphQLで取得する
GraphQL側では、まずサーバーが次のようなスキーマを公開します。
type Query {
articles(first: Int!): [Article!]!
}
type Article {
id: ID!
title: String!
author: Author
}
type Author {
id: ID!
name: String!
}
Queryは読み取りの入口です。first: Int!は必須の整数引数、[Article!]!は一覧自体も各要素もnullにしない型を意味します。author: Authorには!がないため、著者はnullになり得ます。この違いは後でエラーを扱うときに効いてきます。
クライアントは、公開された項目からタイトルと著者名を選びます。
query ArticleList($count: Int!) {
articles(first: $count) {
title
author { name }
}
}
変数に{"count":2}を渡した結果は次の形です。この例はNode.js 22.22.0とGraphQL.js 16.11.0で実行し、応答を確認しています。
{
"data": {
"articles": [
{"title":"Article 1","author":{"name":"Author A"}},
{"title":"Article 2","author":{"name":"Author B"}}
]
}
}
要求で選んだ形がdata以下に現れ、指定していないIDは返っていません。スマホ版で著者名が不要なら、問い合わせからauthorを外せます。既存スキーマ内の項目を組み合わせる変更なら、画面ごとに新しいURLを増やさずに対応できます。
一方、存在しない項目は自由に取得できません。実験では未定義のnonexistentを要求すると、実行前の検証でエラーとなり、dataは返りませんでした。新しい情報を公開するには、サーバー側のスキーマと取得処理の追加が必要です。
| 一覧の作り方 | 画面側の要求 | サーバー側で用意するもの |
|---|---|---|
| RESTで個別取得 | 記事を取得し、著者を追加取得 | 記事と著者の各リソース |
| RESTで集約 | 著者込みの一覧を1回取得 | 集約した表現、またはinclude等の仕様 |
| GraphQL | 記事と著者名を選択して要求 | 型・関連・項目ごとの取得処理 |
不要な項目を多く返すことをオーバーフェッチと呼びます。GraphQLは返却項目を選べますが、裏の取得処理が全列を読む実装ならDB側の仕事は減りません。応答の形とデータ取得の効率は分けて見る必要があります。
1回の要求の裏で、21回取得してしまうことがある
GraphQLでは、各フィールドの値を返す関数をリゾルバーと呼びます。記事一覧のリゾルバーが20件を取得し、各記事の著者リゾルバーが1件ずつ取りに行くと、一覧1回+著者20回になります。これがN+1問題の典型的な形です。
今回、20記事と2人の著者をメモリ内に用意し、同じ問い合わせを二つの実装で実行しました。個別取得では各記事から著者の取得関数を呼びます。一括取得ではDataLoader 2.2.3でIDをまとめ、まとめたIDに対応する著者を返します。
const loader = new DataLoader(async ids => {
calls.authors++;
return ids.map(id => authors.find(author => author.id === id));
});
上は一括取得部分の抜粋です。実DBに置き換えるなら、ここでID群を一括検索し、要求されたIDの順序に対応させて結果を返します。ループ内でSQLを1本ずつ発行すると、まとめた意味がなくなります。
| 実行した実装 | 記事の取得関数 | 著者の取得関数 | 返却データ |
|---|---|---|---|
| 各記事から個別取得 | 1回 | 20回 | 20記事と著者名 |
| DataLoaderでまとめる | 1回 | 1回 | 上と完全一致 |
測ったのは取得関数の呼出回数です。実DBのSQL回数や応答速度のベンチマークではありません。それでも、GraphQLの要求を変えずに、裏の取得処理だけで仕事量が変わることは確認できます。RESTの集約APIでも同じN+1は起きるので、方式名だけで速さは決まりません。
DataLoaderは通常リクエストごとに作り、異なる利用者の結果を共有しないようにします。キャッシュは認可を代行しません。性能を比べるなら同じ画面・件数・権限条件で、HTTP往復、DBアクセス、サーバー処理時間、応答サイズをそれぞれ測ります。
部分的な失敗を、画面でどう扱うか
記事の取得には成功したが、二つ目の著者を返す処理だけが失敗する場合も試しました。応答にはdataとerrorsが同時に含まれました。位置情報を省略すると次の形です。
{
"errors": [{
"message": "Author unavailable",
"path": ["articles", 1, "author"]
}],
"data": {
"articles": [
{"title":"Article 1","author":{"name":"Author A"}},
{"title":"Article 2","author":null}
]
}
}
配列の位置は0から数えるので、1は二つ目の記事です。タイトルは表示でき、著者名だけ取得できません。画面では「著者情報を取得できません」として記事を残すのか、全体を失敗にするのかを決めます。
この例で一部だけ残せたのは、スキーマのauthorがnullを許すためです。nullを許さないフィールドで失敗すると、nullを許す親まで影響が広がります。型を厳しくすることと、部分表示できる範囲は関係しています。
HTTPで配信する場合も、ステータスだけで成功を判断せず応答本文を扱います。HTTPステータスの具体的な選択はエラーの段階やメディアタイプにも関わるため、「GraphQLはすべて200」と覚えない方がよいです。上の実験は実行エンジンの検証で、HTTPサーバーの実験ではありません。
認可・負荷・キャッシュは、自由に取得できる範囲とセットで設計する
スキーマに項目があることは、誰でも読んでよいという意味ではありません。user(id: ...)のIDを変えるだけで他人の非公開情報を取得できないよう、対象レコードやフィールドの認可を業務ロジック側で行います。RESTとGraphQLを併用するなら、同じ権限判断を共有すると食い違いを防ぎやすくなります。
また、first: Int!は整数であることを表すだけで、件数の上限ではありません。実験では取得処理に1〜20件の範囲チェックを入れました。本番では扱うデータ量に合わせ、件数、関連の深さ、別名指定による広がり、処理コストを制限します。浅い問い合わせでも、大量の一覧を要求されれば重くなります。
公開コンテンツのキャッシュ
RESTのGETはURLごとに扱いやすい設計です。GraphQLも読み取りをGETで送る方法がありますが、問い合わせと変数を含む識別方法を設計します。
利用者ごとに異なるデータ
共有CDNに他人の結果を返させない設計が必要です。ブラウザ内のキャッシュと、複数利用者が使う共有キャッシュを分けて考えます。
何が遅いかを調べるログ
GraphQLではURLだけでなく操作名・処理時間・取得回数を追います。変数には個人情報が入り得るため、要求全文を無条件に記録しません。
採用は、今のAPIで困っている画面から判断する
「外部公開APIだからREST」「管理画面だからGraphQL」と用途名だけでは決まりません。利用者が必要とする形の違いと、その柔軟さを運用する負担を比べます。
| 現在の困りごと | 先に試すこと | GraphQLを検討しやすい条件 |
|---|---|---|
| 一つの一覧だけ取得回数が多い | 既存RESTに集約や一括取得を追加 | 同様の画面が多く、要求の組み合わせが継続して変わる |
| Webとモバイルで必要な項目が違う | 項目選択や画面向けAPIの維持費を確認 | 共有する型があり、クライアント側で選ぶ利点が大きい |
| 複数のサービスの情報を組み合わせる | 集約層で認可・タイムアウト・失敗時の表示を設計 | 関連をたどる問い合わせが多く、スキーマを継続管理できる |
| 安定した単純なCRUDを提供している | 既存APIの仕様・監視を整える | 方式変更で解消する具体的な課題が出てから比較 |
最初は、困っている画面を一つ選び、必要な項目と現在の要求回数を書き出します。RESTを改良した案とGraphQL案を並べ、実装量、取得効率、エラー表示、保守担当まで比べると、流行ではなく解消できる課題で判断できます。既存RESTを残し、一部にGraphQLを使う構成も可能です。
REST側の仕様を整理したい場合は、OpenAPI / SwaggerでAPI仕様を共有する方法も役立ちます。
GraphQLとREST APIに関するよくある質問
GraphQLならエンドポイントは必ず一つですか?
単一の入口へ問い合わせる構成が一般的ですが、入口の本数そのものが利点の本体ではありません。サービスや権限境界で分かれることもあります。比較では、必要なデータをどう表現し取得するかを見ます。
更新や削除もGraphQLでできますか?
更新操作にはMutationを定義します。ただし、入力検証、認可、トランザクション、重複実行への対策はサーバー実装の仕事です。Mutationと書けば複数の処理が自動で一つのDBトランザクションになるわけではありません。
GraphQLを入れればN+1は解消しますか?
解消しません。実験のように取得処理をまとめる必要があります。DataLoaderを使う場合も、バッチ関数の内部が個別アクセスのままなら、実DBへの負荷は減りません。
まとめ
GraphQLの強みは、公開された型の中でクライアントが欲しい形を選べることです。その柔軟さに価値がある画面なら有力な選択肢になります。RESTでも集約や項目選択は設計できるため、まず同じ画面を両方式でどう作るか比べ、取得処理と運用まで含めて選びます。
参考情報
公式資料は2026年9月21日に確認。実行例の取得元はメモリ内データです。