REST API 設計 は何をしますか?
API はプロダクトです。コンシューマのメンタルモデルが、データベーススキーマではなく、構造を決定します。このスキルが防ぐ高コストなミスは、一貫性のないサーフェス(命名の混在、間違ったステータスコード、増大するテーブルに対するオフセットページネーション)を出荷してしまい、すべてのコンシューマがそれをハードコードし、統合後は事実上修正不可能になることです。
行動手順
この順序で作業します。リソースはメソッドに先行します。なぜなら、メソッドのセマンティクスはリソースの種類に依存するからです。エラーとページネーションの契約は、仕様が書かれる前に修正します。後付けで修正するとコンシューマが壊れるからです。
ステップ 1: インプットを収集する
設計前に収集します。推測にはすべてラベルを付けます。
- コンシューマ: 内部サービス、ファーストパーティクライアント、または外部開発者。外部コンシューマは一貫性のハードルを上げます。不明な場合は外部をデフォルトとします。
- ドメインのコア名詞と、どの名詞がどの名詞を所有するか(ネスト構造を決める)。
- 想定されるコレクションサイズと成長。約 10,000 行を超える可能性のあるリストは、オフセットページネーションを除外します。
- エンドポイントごとの読み取り/書き込み比率とレイテンシ期待値。
- 認証モデル(401 と 403 の正確性に影響します)。
ステップ 2: リソースを命名する
- 複数形名詞:
/users,/orders,/products。 - 所有関係のために1レベルネストする:
/users/{id}/orders。2レベルより深くネストしないでください。代わりにフィルタで平坦化します (/orders?user_id=...は/users/{id}/orders/{oid}/items/{iid}より優れています)。 - パスに動詞を含めないでください。HTTP メソッドが動詞です。CRUD 動詞に当てはまらない真のアクション(例: 請求書の送信)については、サブリソースへの POST としてモデル化します:
POST /invoices/{id}/send-attempts。 - 一貫したケース: 複数単語のパスセグメントにはケバブケース、JSON フィールドにはスネークケース - 一度選択したら、すべてに適用します。
ステップ 3: メソッドとステータスコードを割り当てる
- GET: 読み取り、べき等、リクエストボディなし。200 とボディを返します。
- POST: 作成。201 と新しいリソースを指す
Locationヘッダーを返します。クライアントが後続の GET をスキップできるように、作成された表現をボディで返します。 - PUT: 完全置換、べき等。欠落フィールドは「保持」ではなく「クリア」を意味します。
- PATCH: 部分更新(JSON Merge Patch または JSON Patch - 仕様でどちらかを明記します)。
- DELETE: 削除、べき等、204 を返します。既に削除されたリソースの削除は 204 または 404 を返します - 1つを選択して文書化します。
- エラー: 不正な入力には 400、資格情報の欠落/無効には 401、有効な資格情報だが権限がない場合は 403、リソース不在には 404(存在を明かすことが漏洩となる場合は 403 の代わりに 404 も使用)、状態の競合には 409、形式は正しいが意味的に無効な入力には 422、レート制限には 429(
Retry-After付き)、サーバー障害には 500 - スタックトレースや内部識別子を決して公開しないでください。
ステップ 4: エラー契約を修正する
すべてのエラーに対して1つの構造、機械可読なコードを最初に:
{
"error": "validation_error",
"message": "Email is required",
"field": "email",
"request_id": "req_abc123"
}
request_id はサーバー側でログに記録され、サポートが関連付けできるようにする必要があります。人間向けメッセージは開発者がログを読むためのものであり、エンドユーザー表示用ではありません。
ステップ 5: ページネーションとフィルタリングを修正する
- 成長するコレクションにはカーソルベースのページネーション: 不透明なカーソルと
has_moreフラグを返します。オフセットページネーションは、小さく、境界があり、管理画面向けのリストにのみ許容されます。これは線形に劣化し、同時書き込みの下で行をスキップ/重複します。 - デフォルトのページサイズは 25、最大は 100。最大を超えるページサイズは 400 で拒否し、静かにクランプしないでください。
- フィルタリングとソートはクエリパラメータ経由:
?status=active&sort=created_at:desc。どのフィールドがフィルタリング可能かを文書化します。不明なフィルタパラメータは無視するのではなく 400 で拒否します(静かな無視はクライアントのバグを隠します)。
ステップ 6: パフォーマンスとペイロード予算を適用する
- p95 レイテンシ予算: インタラクティブエンドポイントは 300 ミリ秒、レポート/エクスポートは 1 秒。操作が 1 秒を満たせない場合は、非同期にします: 202 Accepted とポーリング用のステータスリソースを返します。
- レスポンスペイロード予算: 典型的なレスポンスは 100 KB 未満に保ちます。1 MB を超えるものは、ページネーション、フィールド選択 (
?fields=)、または専用のエクスポートフローが必要です。 - 破壊的変更は URL パス (
/v1/) でバージョン管理します。追加的な変更(新しいオプションフィールド)はバージョンアップを必要としません。非推奨のタイムライン自体はapi-versioning-strategistに委ねます。
対比例
悪い例:
POST /getUserOrders?userId=42&page=3
→ 200 { "data": [...], "error": null }
パスに動詞、POST に GET のセマンティクス、オフセットページング、200 でエラーフィールドをラップしているため、クライアントは呼び出しが失敗したかどうかを知るためにボディを解析する必要があります。
良い例:
GET /users/42/orders?status=shipped&limit=25&cursor=eyJpZCI6OTh9
→ 200 { "orders": [...], "next_cursor": "eyJpZCI6MTIzfQ", "has_more": true }
→ 404 { "error": "not_found", "message": "User 42 does not exist", "request_id": "req_9f2" }
名詞パス、所有権のネスト、カーソルページング、ステータスコードが結果を伝えます。
成果物
以下の内容を含むエンドポイント仕様を作成します: リソーステーブル(パス、メソッド、メソッドごとのステータスコード)、共有エラー構造、ページネーション契約(カーソル形式、デフォルト、最大値)、フィルタリングホワイトリスト、エンドポイントごとのレイテンシ/ペイロード予算。
やってはいけないこと
- API でデータベーススキーマをミラーリングしないでください。コンシューマは変更したくなる内部構造に結合されます。
- エラーボディを持つ失敗に対して 200 を返さないでください。クライアントと監視はどちらもステータスコードをキーにします。
- 無制限のコレクションにオフセットページネーションを使用しないでください。書き込み下で行を重複・スキップし、線形に劣化します。
- パスを 2 レベルより深くネストしないでください。深いネストは1つのアクセスパスをエンコードし、他のすべてを禁止します。
- 不明なクエリパラメータを静かに無視しないでください。フィルタのタイプミスがフィルタリングされていないデータを返すのは、データ漏洩クラスのバグです。
品質基準
- コンシューマが質問せずに仕様からクライアントを実装できること。
- すべてのエンドポイントがエラーケースを含むステータスコードを明記していること。
- すべてのコレクションエンドポイントが1つのページネーション契約を共有し、すべてのエラーが1つの構造を共有していること。
- 同一の概念がすべてのパスとペイロードで同一の名前を持っていること。
- 各エンドポイントに明示的なレイテンシ予算があり、1 秒を超えるものは非同期であること。