Part 2 では成功時のレスポンスを扱った。今回はエラーを設計する。
私が担当した API では、エラーレスポンスを何度も設計し直した。最初は数字のコードで、エンドポイントごとにも形が違かった。最初から次の形で決めておけばよかった。
決めることは 3 つある。どのステータスコードを返すか、エラーコードにどんな名前を付けるか、その定義をどこに書くか。
成功時のステータスコード
操作の種別で決まる。
- コレクションに対する
POST(リソースの新規作成):201 Created - それ以外の成功(取得・更新・削除・アクション):
200 OK
204 No Content は使わない。削除も削除済みオブジェクトを返すので、成功レスポンスは必ずボディを持つ。Stripe のステータスコード一覧も成功は 200 だけで、201 と 204 を含まない。作成だけを 201 にするのは独自の判断である。
エラーは error オブジェクトで返す
{
"error": {
"code": "notification_category_not_found",
"message": "指定されたカテゴリが見つかりません"
}
}
-
code: 機械可読なエラーコード -
message: 人間向けの説明
トップレベルに並べず、error オブジェクトに入れる。 成功レスポンスのフィールドと名前が衝突せず、クライアントは error の有無だけで成功と失敗を分岐できる。Stripe と Google も入れ子にしている。
バリデーションエラーは field_errors を足す。errors という名前にしないのは、何の複数形なのかが読めないためである。
{
"error": {
"code": "validation_failed",
"message": "入力内容に誤りがあります",
"field_errors": {
"email": ["メールアドレスが不正です"],
"address.city": ["50 文字以内で入力してください"],
"phone_numbers[1].number": ["数字のみで入力してください"]
}
}
}
キーはリクエストボディのフィールドへのパスを平坦に並べる。 ネストは .、配列は [n] の添字で表し、ボディの構造をそのまま入れ子にしない。
- クライアントは
field_errors["address.city"]の 1 回の参照で入力欄に対応付けられる。field_errors.address?.cityのような存在チェックの連鎖が要らない - 配列の何番目が不正かを表せる
-
Google AIP-193 の
fieldViolationsもemail_addresses[3].type[2]のパス、Stripe のparamもline_items[0][currency]と、いずれも平坦なパス文字列である
field_errors を [{ "field": "...", "code": "...", "message": "..." }] の配列にする設計もある。違反の種類でクライアントが UI を変えるなら要素ごとに code を持たせられる配列を、サーバーが返した message をそのまま表示するだけならキー引きで済むオブジェクトを選ぶ。
ステータスコードの使い分け
-
404 Not Found: パスパラメーターが指すリソースが存在しない -
409 Conflict: リソースの現在の状態と矛盾して、操作できない -
422 Unprocessable Entity: 入力は構造的に有効だが、業務ルールやバリデーションに違反している
404 はパスパラメーターに使う
GET /v1/notifications/not-a-uuid → 404
GET /v1/notifications/<存在しない ID> → 404
GET /v1/notifications/<削除済みの ID> → 404
パスパラメーターはリソースの識別子である。 フォーマットが不正でも、存在しなくても、削除済みでも、その識別子でリソースを取り出せないという結果は同じなので、区別せず 404 を返す。
バリデーションエラーは 422 を返す
ボディとクエリは識別子ではなく操作データである。フォーマット不正も、存在しないリソースへの参照も、業務ルール違反も 422 にする。
POST /v1/notifications { "category_id": "not-a-uuid" } → 422
POST /v1/notifications { "category_id": "<存在しない ID>" } → 422
ボディで存在しないリソースを指定したときに 404 を返さないのは、URL が指しているコレクションは存在するからである。404 はパスパラメーターの結果だけを表す。
400 Bad Request は JSON としてパースできない場合に限る。パースできたリクエストの中身の問題は 422 に寄せる。
バリデーションエラーを 400 にする設計
入力起因のエラーをすべて 400 にする設計も正しい。400 は「リクエストを受け付けられない」の汎用ステータスで、クライアントの分岐が 1 つ減る。
代わりに「JSON が壊れている」と「値が業務ルールに反する」が同じステータスになり、切り分けはエラーコードに委ねられる。422 を選ぶのは、この 2 つをステータスだけで分けたいからである。
409 は状態の衝突に使う
配信が開始された通知を更新しようとした、確定済みの注文をもう一度確定しようとした。入力そのものは正しく、リソースの現在の状態が操作を許さない場合が 409 である。
エラーコードは状態を表す
notification_not_editable # 操作ベース
notification_delivery_started # 状態ベース
コードには操作を妨げている状態を書き、操作は書かない。
- HTTP メソッドが操作を表すので、エラーコードが操作を繰り返す必要はない
- 「なぜエラーになるのか」が名前から分かる
- 業務ルールが変わっても名前の意味が安定する。「配信開始後は編集も削除もできない」に変わっても
notification_delivery_startedはそのまま使える
操作ベースの名前も許容できる。クライアントは受け取ったコードをそのまま画面の分岐に使える。ただしルールが増えると not_editable と not_deletable が並び、同じ状態に複数のコードができる。
コードは小文字の snake_case で、モジュールのプレフィックスを付ける。notification_*、order_*、payment_*。名前の衝突を避け、コードを見ただけでどのモジュールのエラーか分かる。
カタログを 1 か所に持つ
- コード・ステータス・説明はエラーコード一覧に一元管理する
- 「いつ返るか」のトリガー条件は各 API 仕様書に業務ルールとして書く
- 仕様書の「エラーレスポンス」セクションはカタログへのリンクだけにする
読み手は業務ルールからエラーの意味を理解し、コードを見たときにカタログを引く。 この 2 段構えにすると、同じ説明を 2 か所に書かずに済む。
各仕様書にコードを列挙する方式も、1 ファイルを読めば完結する点で読みやすい。コードが増えると同じ説明が複数の仕様書に散り、片方だけが更新される。
次回は、ここまでの設計を仕様書としてどう書くかを扱う。