⚖️

GraphQLを採用するか迷ったときに読む記事

に公開

記事を書いた目的・動機

GraphQLはREST APIの上位互換ではなく、「使いどころを見極めて使えば便利」「使いどころを間違えるとしんどい」系の技術だと思っています。

「じゃあどういう時に使うべきなの?」と言われたときに、一言で答えるのは難しいので、今後のために記事にまとめようと思いました。

GraphQLのメリット

1. バックエンドの変更なしで、取得するデータを柔軟に変更できる

  • 必要なフィールド・リレーションをクエリ側で柔軟に指定でき、軽微なUI変更ならバックエンド改修や依頼を待たずに進められる。
  • バックエンドはスキーマ/リゾルバが既にあれば、クエリを組み替えるだけで対応できるケースが多い(新フィールドが要る場合のみ型・リゾルバ追加)。
# 取得したい項目が変わった時、フロントエンドでクエリを変えるだけで済む
query {
  document(id: 123) { id title }
  group(id: "1") { id name membersCount }
}

2. 過剰フェッチやリクエスト回数を減らせる

  • 必要なフィールドだけ取得し、複数リソースも1リクエストに束ねられる。
  • リクエスト回数が減るので、回線が遅い/RTTが大きい環境(モバイル・海外拠点・VPN越し等)では体感が速くなりやすい。
  • 間違えて機密情報を返しちゃった、みたいな事故も起こりにくくなる
# RESTなら /documents/123 と /groups/1 を2回呼ぶケース
query {
  document(id: 123) { id title }
  group(id: "1") { id name membersCount }
}

3. バックエンドで定義した型をフロントに共有しやすい

  • スキーマがそのまま型定義となり、Codegenでフロントの型を自動生成できる。
# 例: GraphQL Code Generator
npx graphql-codegen --config codegen.yml
// 自動生成された型で安全にクエリを叩く
const { data } = useGetDocumentQuery({ variables: { id: "123" } });

4. スキーマが自己記述的で、周辺ツールが充実している

  • GraphQLはIntrospection(スキーマをAPI経由で問い合わせる仕組み)が標準で、スキーマが常に取得できる前提。補完・型・差分検知・利用計測などをツールで一気通貫に回しやすい。
    • REST APIでもOpenAPIをきっちり整備すれば似た体験は可能だが、整備されていなければツールが活きにくい
  • よく使われるツール例:
    • GraphiQL / GraphQL Playground / Altair / Insomnia: クエリ試し打ち・補完。
    • Apollo Studio: スキーマレジストリ、Schema Diff、トレース可視化、Persisted Query管理。
    • GraphQL Voyager: スキーマのリレーションをグラフで可視化。
    • GraphQL Code Generator: スキーマからTS型/React Hooksなどを自動生成。
    • graphql-inspector: スキーマ差分検出・破壊的変更チェック。
    • GraphQL ESLint: クエリやスキーマに対するlint。

GraphQLのデメリット・注意点

GraphQLのデメリットは、一言でまとめると「学習コストの高さ」です。

デメリットに挙げられることのほとんどが「工夫次第で回避策はある」のですが、REST APIならほぼ何も考えずにできていたことに対して、頭を使ったり、チームで方針決めたりする必要があります。

またジュニアレベルのエンジニアや、普段フロントエンドを中心に触っているエンジニアがバックエンドのコードを触るハードルが上がります。

1. キャッシュの複雑化

  • REST: リクエスト内容ごとにパスが分かれる(例: GET /documents/123)ので、URL単位でCDN/ブラウザキャッシュを効かせやすい。
  • GraphQL: すべて同じエンドポイントにリクエストが飛び、どのフィールドを返すかはクエリ内容(多くはPOSTボディ)で決まる。
    • クエリ文字列をハッシュ化して短いキーでGET化するPersisted Query、もしくはApollo/Relayの正規化キャッシュでフィールド単位に分解して管理するなど、別途対策/技術選定が必要

2. 監視の複雑化

  • REST: リクエスト内容ごとにパスが分かれる(例: GET /documents/123)ので、URL単位でメトリクスやレート制御を設定しやすい。
  • GraphQL: 1つのエンドポイントに全リクエストが集約するので、パス単位の可視化が効きにくい。フィールドコスト計測や複雑度ベースのレートリミットを自前で入れる必要がある。
    • 例: DataDogやNew Relicで「/graphql のP95が悪化」までは分かるが、どのクエリ/どのフィールドが重いかが見えない。例えば「一覧+関連リレーションを深く取りに行くクエリ」が特定ユーザーから大量に飛ぶと、突如DB負荷が跳ねる。
    • 対策例: クエリ複雑度/深さを計算し閾値超過で拒否する、フィールドごとのコストメトリクスをロギングして可視化する(例: DataDogカスタムメトリクスにfield=documents.authorなどを送る)。
    • レートリミットもパス単位ではなくクエリ内容や複雑度に応じて設計する必要がある。

3. N+1が起こりやすい

  • REST: 返す項目が固定なので、includes(:author) のようにコントローラ側で事前にEager Loadを仕込める。
  • GraphQL: クライアントが「欲しいフィールド」を毎回変えられるため、どこまでEager Loadすれば足りるか事前に読みにくい。ネストを深く取られると簡単にN+1が起こる。
    • 特にリスト×深いネストでフィールドを増やされると、一気にSQL回数が爆発しやすい。
# クエリ例(一覧+author name を取得)
query { documents { id title author { id name } } }
# 典型的なN+1パターン(author フィールドで毎回SQL)
class Types::DocumentType < Types::BaseObject
  field :id, ID, null: false
  field :title, String, null: false
  field :author, Types::AuthorType, null: false

  def author
    object.author # documents件数ぶん SELECT authors... が走る
  end
end

# 対策: Loaderでまとめて取得
class AuthorLoader < GraphQL::Batch::Loader
  def perform(ids)
    Author.where(id: ids).each { |a| fulfill(a.id, a) }
  end
end

class Types::DocumentType < Types::BaseObject
  field :author, Types::AuthorType, null: false
  def author
    AuthorLoader.for.load(object.author_id)
  end
end

4. 記述量、揃えるもの、覚えるべき概念が多い

  • REST: ルーティング + コントローラで完結しやすい。
  • GraphQL: 1リソースでもスキーマ/Type/Query/Mutation/Resolver(+Loader)が必要。
# REST 最小例
# config/routes.rb
resources :documents

# app/controllers/documents_controller.rb
class DocumentsController < ApplicationController
  def index;   render json: Document.all; end
  def show;    render json: Document.find(params[:id]); end
  def create;  render json: Document.create!(document_params), status: :created; end
  def update;  doc = Document.find(params[:id]); doc.update!(document_params); render json: doc; end
  def destroy; Document.find(params[:id]).destroy!; head :no_content; end
  private def document_params; params.require(:document).permit(:title, :body); end
end
# GraphQL で同等CRUDを揃える最小例

# app/graphql/types/document_type.rb
class Types::DocumentType < Types::BaseObject
  field :id, ID, null: false
  field :title, String, null: false
  field :body, String, null: true
end

# app/graphql/types/query_type.rb
class Types::QueryType < Types::BaseObject
  field :documents, [Types::DocumentType], null: false
  field :document, Types::DocumentType, null: true do
    argument :id, ID, required: true
  end
  def documents; Document.all; end
  def document(id:); Document.find_by(id: id); end
end

# app/graphql/mutations/create_document.rb
class Mutations::CreateDocument < Mutations::BaseMutation
  argument :title, String, required: true
  argument :body, String, required: false
  type Types::DocumentType
  def resolve(title:, body: nil); Document.create!(title:, body:); end
end

# app/graphql/types/mutation_type.rb
class Types::MutationType < Types::BaseObject
  field :create_document, mutation: Mutations::CreateDocument
  # update_document, delete_document も別途追加
end

# app/graphql/schema.rb
class Schema < GraphQL::Schema
  query Types::QueryType
  mutation Types::MutationType
end

5. エラーハンドリングがやや煩雑になる

  • REST: ステータスコードで成功/失敗を即判定できる(200/4xx/5xx)。
  • GraphQL: ビジネスエラーや部分的な失敗でもHTTP 200で返る実装が多く、dataerrors が混在する「部分成功」が頻出する。クライアントは毎回 errors を確認しないと正常系と異常系を取り違える。
    • 例: 一覧は取れたが一部フィールドが権限不足で null になり、errors にだけ失敗理由が載る。HTTPステータスは200のまま(Transportエラーやスキーマ未満のバリデーションエラーのみ4xx/5xxになる運用が一般的)。
    • 実装次第ではビジネスエラーを4xxで返す場合もあるが、標準的な運用では200 + errors が多いため、クライアント側は常に errors を解釈する設計が必要。
{
  "data": { "document": { "id": "123", "title": "ok", "secret": null } },
  "errors": [{ "message": "Not authorized", "path": ["document", "secret"] }]
}
  • 結果として、クライアント側の共通エラーハンドリングやロギング設計がRESTより重くなる。

6. ファイルアップロード/ストリーミングの扱いが大変

  • REST: POST /documents/123/filesmultipart/form-data を送るだけで一般的なミドルウェアが処理してくれる。
  • GraphQL: ファイルを送りたい場合、graphql-multipart-request-spec や Apollo Upload など専用のプロトコル・ミドルウェアが必要。サーバ/クライアントの双方で対応を入れないと動かない。
    • 例: クライアントは通常のGraphQLリクエストではなく、operations + map + 実ファイルを含んだmultipartを投げ、サーバ側でgraphql-multipart-request-spec対応ミドルウェアを通してからリゾルバに渡す。
    • 中継CDNや一部クライアントがmultipart仕様に非対応だと、そのままでは動かず別経路や専用クライアントが必要になる。
    • 大容量のストリーミングは、結局REST/S3直PUTや署名URLなど別の経路で処理することが多い。

ハイブリット運用という選択肢について

GraphQLは、読み取りの面での強みが多く、書き込み処理まわりのメリットは少ない。
なので読み取りだけGraphQL使い、書き込みはRESTを使う、というハイブリット運用が採用されることもある。
この場合のメリット、デメリットは以下。

メリット

  • 読み取りはGraphQLで過剰/過少フェッチを抑え、複数リソースも1リクエストにまとめられる。
  • 書き込みをRESTに寄せることで、イデンプテンシキーや強いバリデーション、WAF/CDN/メソッド別レートリミットなど従来運用をそのまま活かしやすい。
  • ファイルアップロードや大容量ストリームなど、REST/multipartや署名URLと相性の良い処理を素直に実装できる。

デメリット・注意点

  • 権限チェックやバリデーションがGraphQLとRESTで二重化しやすい。サービス層などで共通化しないとロジックが分散する。
    • 例: GraphQLのcustomer取得では「一般ユーザーにはemailを返さない(nullにする)」、RESTのcustomer更新PUT /customers/:idでは「管理者ならemailを更新可」という処理になっていたとする。
    • 新しい要望で「特定プランのユーザーにはemailを表示する」に変更したとき、GraphQL側だけ修正してREST側の権限制御を放置すると、表示と更新の仕様が食い違う。逆も然り。共通化しないと片側だけ古い仕様が残る。
  • ドキュメント/スキーマが分裂する
    • 例: GET系はGraphQL SDLを参照、更新系はSwaggerを参照…と2つのポータルを往復しなきゃいけない
  • 一貫性担保が複雑になる
    • GraphQLで読みこんだ上でRESTで書き込む処理を実装する時、楽観ロックやバージョン管理を意識しなきゃいけないケースがある
    • 例: GraphQLで取得した古いupdated_atのままRESTで更新すると上書き事故。If-Matchヘッダやversionを自前で持たせる必要がある。
  • 監視・レートリミット・トレースが2系統になる。ダッシュボードやアラートをどうまとめるかを事前に設計しておく。
    • 例: GraphQLは複雑度ベースのリミットと/graphql単一エンドポイントのP95監視、RESTはパス別QPS制御とアラート、2つのダッシュボードを保守する手間が増える。

GraphQLが向いているプロジェクトの特徴

  • READ中心で、取得したいデータが頻繁に変わるサービス
  • 1つのリソースをたくさんの画面/状況から取得するサービス
  • フロントに型を共有することの重要度が高い(≒フロントが複雑な)サービス
  • モバイル/海外拠点などネットワーク往復時間(RTT)が大きい環境で使われることが多いサービス
  • バックエンドエンジニアとフロントエンド/モバイルエンジニアが密に同期しづらいチーム
  • バックエンドのコードは、GraphQLに慣れたバックエンドエンジニアだけが触るチーム

GraphQLが向かないプロジェクトの特徴

  • READは比較的シンプルで、書き込みの比重が高めのサービス
  • ファイルアップロードや大容量ストリームを多用するサービス
  • キャッシュ最適化、DataLoader設計、監視周りの設計に割ける工数が少なく、かつRESTで十分まかなえる規模・要件のサービス
  • バックエンドエンジニアとフロントエンド/モバイルエンジニアが密に連携できる小規模チーム
  • フロントエンドエンジニアやインターン生など、バックエンドがあまり得意でないエンジニアもある程度バックエンドのコードを触るチーム
  • GraphQLを扱えるエンジニアの安定確保が難しいチーム

その他もろもろ

よくある失敗例

  • 「モダンだから」「GitHubが使っているから」という理由だけで、自社の要件・チーム規模との適合を検討せずに採用する
    • GraphQLはREST APIの上位互換ではない
  • フロントエンドエンジニアが、GraphQLの良い面だけを見て採用を決める
    • GraphQLは基本的に、フロントが楽に、バックエンドが複雑になる技術
    • バックエンドに対してもたらされる複雑性までトータルで考慮して決める必要がある
  • キャッシュ、監視、スロークエリ対策、スキーマ運用、ファイルアップロード周りの処理などを考慮せずに工数計算し、工数が想定より膨らむ
  • 小規模チームで1人のGraphQL経験者が導入を進め、そのエンジニアの退職後にスキーマ設計/運用のレビューが難しくなる
  • 「RESTと並行運用し合わなかったら戻す」としつつ、撤退の判断基準を決めておらず、ズルズル運用して技術的負債が膨らむ

ざっくり年表

  • 2012頃: Facebook社内で利用開始
  • 2015: OSS公開
  • 2016–2018: GitHub API v4採用、Shopify/Contentfulが導入。2018にGraphQL Foundation設立。Apollo/Relayが成熟し始める
  • 2019–2021: Hasura/Prisma/Apollo Federationなどエコシステム拡充。大規模事例が増え、npm DL・検索トレンドはこの時期がピーク
  • 2022–2025: 成熟フェーズ。フェデレーション(複数サブグラフを束ねるアーキテクチャ)や、読み取りをGraphQL・書き込みをRESTに残すハイブリッド採用例が増加。単純なCRUDや書き込み中心のサービスではREST APIに立ち返る動きも目立ち、熱狂は落ち着いて“適材適所”で使われている

参考記事

https://speakerdeck.com/kazukihayase/mosijin-karagraphqlwocai-yong-surunara?slide=56

https://engineering.mercari.com/blog/entry/20220303-concerns-with-using-graphql/

https://zenn.dev/ureo/articles/graphql-overview

https://qiita.com/ymgc3/items/3dbdea2ac487e315b9c2

https://qiita.com/naka_kyon/items/eca9b9d0d369dcdb3f3d

https://qiita.com/yuuman/items/9b32517ab584bbc01062

Discussion