見出し画像

GraphQLのエラー設計について

こんにちは、食べログフロントエンドチームの荒川です。

先日リリースした食べログノートというプロジェクトでは、GraphQLを利用しました。(食べログノートの詳細は後日、別の記事でご紹介する予定です)
今回はその中で、エラーをどう設計したかについてご紹介しようと思います。

HTTPレスポンスとGraphQLのエラー応答について

まず、今回のGraphQLのフロントエンド側のクライアントにはApollo Clientを利用しました。
Apollo Clientのドキュメントには

We recommend using the included Error Codes or Custom Errors for error consistency rather than directly modifying the HTTP response.

Apollo GraphQL Docs - Error handling

HTTP応答を直接変更するのではなく、エラーの一貫性を保つために、付属のエラーコードまたはカスタムエラーを使用することをお勧めします。

Google翻訳

との記載があります。
今回はこれに倣い、HTTPのレスポンスコードは200(Success)のまま、レスポンスBodyのエラーコードでエラーの詳細を表現する方針にしました。

GraphQLのカスタムエラー

では、カスタムエラーとはどのことを指してるか確認します。
GraphQLの仕様をみてみると、errorsフィールドのextensionsが任意に拡張できる領域で、他はエラーの概要を示すフィールドでした。
GraphQL Specification - Errors. Error-result-format

こちらが仕様に記載のある例です。
GraphQL Specification - Errors. example-8b658

{
  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [{ "line": 6, "column": 7 }],
      "path": ["hero", "heroFriends", 1, "name"],
      "extensions": {
        "code": "CAN_NOT_FETCH_BY_ID",
        "timestamp": "Fri Feb 9 14:33:09 UTC 2018"
      }
    }
  ]
}

これを見ていくと、errorフィールドは他に、エラーメッセージを持つmessage、エラーの箇所を特定するpath、locationというフィールドなどがあります。
今回のプロジェクトでは、messageフィールドをシステム的にエラーの内容を表現する箇所とし、ユーザ向けのメッセージはextensions内で表現することにしました。

また、GraphQLの仕様として間違ったリクエストや、GraphQLサーバ障害の場合は200以外のHTTP Statusが返るようにして区別しています。

これらを元に、extensionの内容は以下のように定義しました。

  • code:enum ErrorCode
    エラーの大枠を表すシンボルで、このコードを元にクライアント側の処理を行う。基本的にはバックエンドAPIが返した HTTP エラーステータスコードに対応するcodeを返す。

  • userMessage:String
    ユーザに理解できるメッセージ。特に Mutation のエラー理由で利用する。

  • errorDetails:Array
    バリデーションエラーなど、エラーの詳細が必要な場合に応答する項目。

GraphQLのSchemaでは、拡張したエラーの形式について定義できないため、バックエンドとフロントエンドで予め仕様を取り決めておくことが必要になります。

実際の応答の例

GraphQLのMutationで、入力値のValidationエラーが発生したと想定し、具体例をみていきましょう。
(Objectの型定義はこちらの通りです GraphQL - GraphQLError

// GraphQLErrorの例
{
  "message": "Invalid data inputted."
  "extensions": {
    "code": "UNPROCESSABLE_ENTITY",
    "userMessage": "入力項目にエラーがあります",
    "errorDetails": [
      {
        "message": "hoge@example.com はすでに存在します",
        "attribute": "email"
      },
      {
        "message": "お名前の入力は必須です",
        "attribute": "lastName"
      }
    ],
  }
}

通常、HTTPのレスポンスコードで422(UNPROCESSABLE_ENTITY)が返却される場合は、データが何かしらの制約で処理できない時に発生するもののため、バリデーションエラーの場合は常にこのコードを返すようにしました。
MDN - 422Unprocessable Entity

クライアント側ではuserMessageと、errorDetailsの項目を元に、入力フォームに対してエラーを設定していきます。
前回の記事でご紹介した通り、入力フォームにはReact Hook Formを利用しているので、setError(ReactHookForm - setError)で応答されたerrorDetailsの内容をマッピングします。

実装した結果

ここまでの内容を実装し、ブラウザ上で動作確認した結果です。

Chromeデバッガーで表示したエラーレスポンスのBody
ブラウザ上の画面例

意図した通りにエラーが表現できました!

まとめ

今回はGraphQLを使った私たちのプロジェクトにおけるエラー設計についてご紹介しました。

設計するにあたっては、

  • GraphQLの標準仕様では細かいエラー表現が難しく、プロジェクトに沿った形でエラー応答の詳細を設計する必要がある。

  • GraphQLにはエラー応答を拡張できる領域があり、レスポンスの一貫性を保つため、Apollo Clientではこの領域を利用したカスタムエラーを返すことを推奨している。

というのがポイントでした。

最後に

現在、食べログではフロントエンドに関わるポジションとして以下の2つを募集しています。

気になった方は是非チェックしてみてください!

フロントエンド統括チームに所属するフロントエンドエンジニア
フロントエンドをメインにサービス開発を担当していくWEBエンジニア

どれかに当てはまった方は以下のリンクも是非御覧ください!

この記事が気に入ったらサポートをしてみませんか?