開発2026-04-14· 9 min

6ヶ月後に後悔しないREST APIの設計。

バージョニング、ページネーション、読めるエラー、冪等性。おもちゃと本番品を分ける退屈な詳細。

APIは突然壊れるのではなく、徐々に劣化する。最初は綺麗で、エンドポイントが三つ、スキーマがナプキン一枚に収まる規模で始まる。六ヶ月後には、誰が追加したか覚えていないフィールド、嘘をつくステータスコード、リソースごとに一貫しないページネーション、誰も維持しない変更履歴を抱えている。六ヶ月後に憎まないAPIを設計するのは、優雅さの問題ではなく、最初に退屈な決定を下し、規律をもって守り続ける問題だ。

初日からの明示的なバージョニング。 真剣な選択肢は二つある。パスにバージョンを含める方式(/v1/orders)か、Acceptヘッダに含める方式(application/vnd.kimox.v1+json)だ。パスは運用上の明瞭さで勝つ。ログ、メトリクス、Sentryのトレースに可視で、クライアントもドキュメントなしで理解する。ヘッダはコンシューマに応じて同じリソースの契約が変わる場合、典型的には複数の公式SDKと長いライフサイクルを持つプラットフォームで勝つ。九五パーセントのケースでは、パスの/v1が正解だ。重要なのはどちらかではなく、最初のクライアントが本番に出る前にコミットすることだ。

ページネーション、デフォルトはカーソル、オフセットは必要なときだけ。 オフセットは小さな管理画面のリストには楽だが、大規模データセット、並行書き込み、厳密に安定でない順序では破綻する。単調なフィールド(タイムスタンプとid)に基づく不透明なカーソルは、挿入に強く、無限フィードに対応し、レコードを数える必要がない。デフォルトのページサイズは25、最大100、レスポンスに明示的なnext_cursorを含める。総件数の計算が高価な場合は返さず、正直なhas_moreを選ぶ。

嘘をつかないエラー。 RFC 7807(application/problem+json)を採用し、type、title、status、detail、ログと相関可能なinstanceを含める。422は422、409は409、404は決して{"error": "not found"}を伴う200ではない。エラー本体を200で返す判断こそ、最も多くのクライアントを壊す。リトライを壊し、サーキットブレーカを壊し、成功率メトリクスを壊す。HTTPコードは存在する、私たちはそれを使う。バリデーションエラーには、対象フィールドへのJSON Pointerを持つerrors配列を含める。

金銭や外部状態を変えるすべてのPOSTに冪等性。 決済作成、トランザクショナルメール送信、外部システム呼び出しではIdempotency-Keyを必須ヘッダとして受け入れる。元のレスポンスをそのキーで24時間索引付けして保存し、リトライ時にそのまま返す。これは受信Webhookでは選択肢ではない。ネットワークは落ち、プロバイダはリトライし、冪等性がなければ二重課金が起きる。キーはクライアントが生成し、通常は論理試行ごとのUUIDv4だ。

バロックな装飾なしの命名とリソース。 名詞の複数形(/invoices、/customers)、パスに動詞は決して置かない。ネストの最大深度は二つまで。/customers/{id}/invoicesは良い、/customers/{id}/invoices/{id}/lines/{id}/taxesは良くない。CRUDに収まらない場合は、/cancelOrderを発明する代わりに、POSTで/orders/{id}/cancellationのようにサブリソースとしてモデル化する。フィルタ、ソート、検索はクエリ文字列に置く、パスには置かない。?status=paid&sort=-created_atのように。

加法的な進化だけ。 オプションフィールドと新しいエンドポイントの追加は誰も壊さない。フィールドの改名、削除、型変更は壊す。変更が必要なときはexpand-then-contractを適用する。新しいフィールドを追加し、クライアントを移行させ、その後に古い方をDeprecation: trueヘッダとSunsetのISO日付付きで廃止する。外部クライアントに対する非推奨期間は六ヶ月を下回らない。

ドキュメントはコードだ。ハンドラから生成されたOpenAPI、実行可能な例、同じリポジトリにバージョン管理された変更履歴。ドキュメントが別の場所に住んでいるなら、それは嘘をついている。