📝

なぜGraphQLはエラーでも200を返すのか?RESTとの設計思想の違いを理解する

に公開

はじめに

GraphQLを学び始めたとき、多くの人がこんな疑問を持つのではないでしょうか。

「なんでエラーが起きてるのに200 OKが返ってくるの?」

RESTfulなAPIに慣れていると、これは直感に反する動作に見えます。404や500が返ってこないと、エラーハンドリングどうするの?と困惑しますよね。私もGraphQLでのエラー設計時に困惑した記憶があります...

この記事では、GraphQLとRESTのエラーハンドリングの違いを掘り下げながら、なぜGraphQLがこのような設計になっているのかを初学者向けに解説します。

RESTの世界:HTTPステータスコードが主役

まず、RESTfulなAPIのエラーハンドリングを振り返りましょう。

RESTでは、HTTPステータスコードがすべてを語ります。

GET /users/123
→ 200 OK(成功)

GET /users/999
→ 404 Not Found(ユーザーが存在しない)

POST /users(不正なデータ)
→ 400 Bad Request(リクエストが不正)

POST /users(サーバー障害)
→ 500 Internal Server Error(サーバー側の問題)

クライアント側のコードもシンプルです。

// RESTの典型的なエラーハンドリング
const response = await fetch('/api/users/123');

if (response.ok) {
  const user = await response.json();
  // 成功処理
} else if (response.status === 404) {
  // ユーザーが見つからない
} else if (response.status >= 500) {
  // サーバーエラー
}

この設計には大きなメリットがあります。

  • 一目で結果がわかる:ステータスコードを見れば成功/失敗が即座に判断できる
  • 標準化されている:HTTPの仕様に則っているため、誰もが理解できる
  • ツールとの親和性:監視ツールやロードバランサーがステータスコードを理解できる

GraphQLの世界:部分的成功という概念

では、GraphQLはなぜ違うアプローチを取るのでしょうか?

キーワードは 「部分的成功(Partial Success)」 です。

GraphQLでは、1回のリクエストで複数のデータを同時に取得できます。

query {
  user(id: "123") {
    name
    email
  }
  posts(userId: "123") {
    title
    content
  }
  notifications {
    message
  }
}

このとき、こんな状況が起こりえます。

  • user → 成功
  • posts → 成功
  • notifications → 失敗(通知サービスが一時的にダウン)

この場合、HTTPステータスコードは何を返すべきでしょうか?

  • 200? → 一部失敗してるのに?
  • 500? → 2/3は成功してるのに?
  • 207 Multi-Status? → RESTではあまり使われないし、複雑...

どれも正確ではありません。

だからGraphQLは、HTTPステータスコードに頼らず、レスポンスボディで詳細を伝える設計を選びました。

{
  "data": {
    "user": { "name": "田中太郎", "email": "tanaka@example.com" },
    "posts": [{ "title": "GraphQL入門", "content": "..." }],
    "notifications": null
  },
  "errors": [
    {
      "message": "Notification service unavailable",
      "path": ["notifications"],
      "extensions": {
        "code": "SERVICE_UNAVAILABLE"
      }
    }
  ]
}

HTTPステータスは200。でも errors 配列を見れば、何が失敗したか正確にわかります。

2つの設計思想を比較する

RESTとGraphQLのアプローチを整理してみましょう。

観点 REST GraphQL
エラー通知の主役 HTTPステータスコード レスポンスボディのerrorsフィールド
1リクエストの粒度 1リソース 複数リソース可
部分的成功 基本的に想定しない 明示的にサポート
エラーの詳細度 ステータスコード+ボディ errors配列で構造化

HTTPレイヤーとアプリケーションレイヤーの分離

GraphQLの設計で重要なのは、責務の分離という考え方です。

HTTPレイヤー(トランスポート層)の責務

  • リクエストがサーバーに届いたか
  • サーバーが応答できる状態か
  • 認証は通ったか

GraphQLレイヤー(アプリケーション層)の責務

  • クエリの構文は正しいか
  • 要求されたデータは存在するか
  • ビジネスロジック上のエラーはないか

GraphQLの考え方では、「リクエストを受け取り、GraphQLとして処理できた」時点でHTTPとしては成功(200)。その処理結果がエラーを含むかどうかは、アプリケーション層の話なので errors で表現する、というわけです。

GraphQLでも200以外を返すケース

とはいえ、GraphQLでも200以外を返すことはあります。

POST /graphql(構文エラー)
→ 400 Bad Request
   GraphQLのパース自体ができないケース

POST /graphql(認証なし)
→ 401 Unauthorized
   GraphQL処理に入る前に弾かれるケース

POST /graphql(サーバー障害)
→ 500 Internal Server Error
   GraphQLエンジン自体が動作不能なケース

ポイントは、GraphQL処理に入る前のエラーはHTTPステータスで返すということ。GraphQL処理に入った後のエラーは errors フィールドで返します。

クライアント側の実装:RESTとの違い

この設計の違いは、クライアント側の実装に大きく影響します。

RESTの場合

// ステータスコードをチェックすればOK
if (!response.ok) {
  throw new Error(`HTTP Error: ${response.status}`);
}
const data = await response.json();

GraphQLの場合

// ステータスコードだけでは不十分!
const response = await fetch('/graphql', {
  method: 'POST',
  body: JSON.stringify({ query: '...' })
});

const result = await response.json();

// errorsフィールドも必ずチェック
if (result.errors && result.errors.length > 0) {
  // エラー処理
  // ただし result.data にも有効なデータがあるかも!
  console.error(result.errors);
}

// dataも使える可能性がある
if (result.data) {
  // 成功した部分のデータを活用
}

GraphQLではステータスコードとerrorsの両方をチェックする必要があります。これを忘れると、エラーが起きているのに気づかず処理を続けてしまう、というバグの原因になります。

実務でのベストプラクティス

1. Apollo Clientなどのライブラリを使う

主要なGraphQLクライアントライブラリは、このあたりをうまく抽象化してくれます。

// Apollo Clientの例
const { data, error, loading } = useQuery(GET_USER);

if (error) {
  // errorsが自動的にここに来る
}

2. エラーの種類を構造化する

サーバー側で extensions を活用し、エラーの種類を明確にしましょう。

{
  "errors": [{
    "message": "User not found",
    "extensions": {
      "code": "NOT_FOUND",
      "field": "user"
    }
  }]
}

3. 部分的成功を活かす

GraphQLの強みは、一部が失敗しても他の部分は使えること。UIでは「通知の読み込みに失敗しました」と表示しつつ、他の情報は正常に表示する、といった実装が可能です。

まとめ

GraphQLがエラー時も200を返す理由は、HTTPステータスコードでは表現しきれない「部分的成功」をサポートするためです。

設計思想 REST GraphQL
考え方 HTTPステータス = 結果のすべて HTTP = 通信の成否、アプリ = ビジネスロジックの成否
メリット シンプル、直感的 柔軟、部分的成功をサポート
注意点 複数リソース取得時に複数リクエスト必要 errorsフィールドのチェックを忘れずに

どちらが優れているというわけではなく、解決しようとしている問題が違うのです。

RESTは「1リクエスト1リソース」の世界で最適化されており、GraphQLは「1リクエストで複雑なデータ取得」の世界で最適化されています。

この違いを理解しておくと、GraphQLのエラーハンドリングも自然に受け入れられるようになるはずです。

参考資料

Discussion