なぜ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