1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

API 設計ガイド Part 3 - エラーの設計

1
Posted at

Part 2 では成功時のレスポンスを扱った。今回はエラーを設計する。

私が担当した API では、エラーレスポンスを何度も設計し直した。最初は数字のコードで、エンドポイントごとにも形が違かった。最初から次の形で決めておけばよかった。

決めることは 3 つある。どのステータスコードを返すか、エラーコードにどんな名前を付けるか、その定義をどこに書くか。

成功時のステータスコード

操作の種別で決まる。

  • コレクションに対する POST(リソースの新規作成): 201 Created
  • それ以外の成功(取得・更新・削除・アクション): 200 OK

204 No Content は使わない。削除も削除済みオブジェクトを返すので、成功レスポンスは必ずボディを持つ。Stripe のステータスコード一覧も成功は 200 だけで、201204 を含まない。作成だけを 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-193fieldViolationsemail_addresses[3].type[2] のパス、Stripe の paramline_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_editablenot_deletable が並び、同じ状態に複数のコードができる。

コードは小文字の snake_case で、モジュールのプレフィックスを付ける。notification_*order_*payment_*。名前の衝突を避け、コードを見ただけでどのモジュールのエラーか分かる。

カタログを 1 か所に持つ

  • コード・ステータス・説明はエラーコード一覧に一元管理する
  • 「いつ返るか」のトリガー条件は各 API 仕様書に業務ルールとして書く
  • 仕様書の「エラーレスポンス」セクションはカタログへのリンクだけにする

読み手は業務ルールからエラーの意味を理解し、コードを見たときにカタログを引く。 この 2 段構えにすると、同じ説明を 2 か所に書かずに済む。

各仕様書にコードを列挙する方式も、1 ファイルを読めば完結する点で読みやすい。コードが増えると同じ説明が複数の仕様書に散り、片方だけが更新される。

次回は、ここまでの設計を仕様書としてどう書くかを扱う。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?