Skip to content
Published on

API 設計完全ガイド: 戻せない決定から先に決める

シェア
Authors

はじめに

API 設計で本当に危険なのは難しい決定ではなく、戻せない決定です。キャッシュ戦略を間違えれば次のスプリントで直せます。識別子の形式を間違えれば三年後でも直せません。どちらも「設計」という一語でくくられているので、多くのチームが同じだけの時間をかけます。

このブログにはすでに API デザイン完全ガイド — REST・OpenAPI・Versioning・Pagination・Idempotency・Webhook があり、レートリミットのアルゴリズムを選ぶ のようにゲートウェイ層を扱った記事もあります。それらは広い目録です。本記事は同じ材料を戻すコストという一本の軸で並べ直します。広さではなく深さです。何を今日確定すべきで何を後回しにしてよいか、そして確定するときに仕様の原文が実際に何と言っているかを見ます。

根拠は RFC 9110(HTTP Semantics)、RFC 6585(追加ステータスコード)、RFC 9457(Problem Details)、そしてマーチン・ファウラーの Parallel Change です。


1. 戻せないものと戻せるもの — 判別基準

1-1. 四つの判別質問

戻せるかどうかは技術的な難しさではなく、クライアントがその値と結ぶ関係で決まります。

  1. クライアントがこの値を自分のストレージに保存するか?
  2. クライアントがこの値でコード分岐をするか?
  3. この値が他システムのキーやログ、精算の根拠になるか?
  4. この値を変えるとクライアントのコード修正と再デプロイが要るか?

一つでも「はい」なら、戻せない決定として扱います。

1-2. 実際の一覧

決定戻すコスト理由
リソース境界と URL 構造非常に高いクライアントコードに埋め込まれる。リダイレクトでも消えない
識別子の形式と意味非常に高い外部システムが保存しインデックスする
メソッド意味論非常に高い再試行・キャッシュ・プロキシの挙動が結びつく
ステータスコードとエラー識別子高いクライアントの分岐条件
ページネーション契約高いカーソル形式とレスポンス封筒が一緒に固まる
時刻・金額・列挙の表現高いパースコードと保存スキーマに反映される
認証方式高いすべてのクライアントのデプロイが必要
レートリミットの数値下げれば破壊的、上げれば安全
レスポンスへのフィールド追加低い未知のフィールドを無視するなら安全
内部実装・ストレージ・性能低い契約が保たれるかぎり自由

1-3. ドキュメントになくても契約になるもの

書いていない挙動も、クライアントが依存し始めれば契約になります。並び順を明示していないのにクライアントが「だいたい新しい順」に合わせて UI を作ったなら、順序を変えた瞬間に障害として報告されます。対処は明示しないという事実を明示することだけです。

  • 「並び順は保証しません。順序が必要なら sort パラメータを使ってください。」
  • 「未知のフィールドは無視してください。フィールドは予告なく追加されます。」
  • 「この列挙には新しい値が追加されます。未知の値の扱いは以下に従ってください。」

この三文を v1 のドキュメントに入れるコストは 10 分で、入れなければ v2 を作ることになります。

1-4. だから決める順番

最初のリリース前に確定すべきなのは上の表の上位五行です。残りは最初の利用者が付いてから決めても遅くありません。逆の順で働くチームが多くあります。レスポンスのフィールド名を二時間議論し、識別子の形式は 15 分で決めます。


2. リソース境界と識別子

2-1. 境界は組織図ではなくクライアントの名詞

リソースをチーム境界どおりに切ると、組織改編のたびに API が揺れます。基準はクライアントが認識する名詞です。クライアントが「注文」という一つの概念で扱うものをサーバ内部の都合で三つに割ると、クライアントは三つを合わせるコードを書き、その規則が事実上の契約になります。検証の質問は三つです。単独で取得する理由があるか、寿命が親と違うか、別の権限境界があるか。三つとも「いいえ」なら、それは親リソースのフィールドです。

2-2. URL 構造が固まる地点

例 — パステンプレートは必ずコードブロックの中に置きます。

GET    /v1/orders/{orderId}
GET    /v1/orders/{orderId}/items
POST   /v1/orders/{orderId}/cancellations
GET    /v1/customers/{customerId}/orders?status=paid&limit=50

ネストは二段までが実用上の限界です。/v1/customers/:customerId/orders/:orderId/items/:itemId:itemId が全体で一意なら前半が装飾で、装飾はタイプミスと 404 を生みます。

状態遷移の表現もここで決まります。キャンセルを POST /v1/orders/:orderId/cancellations のように下位リソースの作成と見ることも、ステータスフィールドの部分更新と見ることもできます。前者は履歴がリソースとして残り冪等性キーを付けやすく、後者はエンドポイント数が少なくて済みます。一つの API の中で混ぜることだけ避ければ十分です。

2-3. 識別子の三つの選択肢

方式列挙可能性規模の露出ソート可能インデックス局所性
連番整数高い露出するはい良い
ランダム UUID低いなしいいえ悪い
時刻順 ID低い部分的はい良い

連番整数を外に出すと、競合他社が一日おきに二回呼ぶだけで日次注文量を推定でき、攻撃者は識別子をたどりながら権限チェックを試せます。

2-4. 識別子に意味を入れない

ORD-2026-KR-000123 のような識別子の問題は、クライアントがこれをパースすることです。国コードが三桁になる日、パースコードが全部壊れます。接頭辞をどうしても使うなら、ord_ のように型だけを示す固定接頭辞に限ります。内部識別子と外部識別子を分ける選択肢もあります。コストはマッピングテーブル一つ、利得はストレージ入れ替えが API に漏れないことです。

2-5. 論争点 — REST と RPC・GraphQL の境界

業界の意見が割れる場所です。勝者ではなく軸を見ましょう。

  • クライアントの多様性: 自社ウェブアプリ一つならサーバが画面に合わせて応答を作るほうが効率的で、多くて統制できないならリソース中心の一般的な契約が有利です。
  • キャッシュの要求: HTTP キャッシュ基盤を使うにはリソースとメソッド意味論が要ります。単一エンドポイントに POST でクエリを送る方式はこの層を手放します。
  • 組織の境界: 契約を受け取る側が外部組織なら、自己記述的でドキュメント化しやすいほうが有利です。
  • クエリ形状の変動性と運用の複雑さ: 必要なフィールドの組み合わせが変わり続けるならクエリ言語の利得は大きいものの、コスト上限と深さ制限とキャッシュ戦略を新たに設計する必要があります。

大事なのは境界をドキュメントで決めることです。


3. メソッド意味論: safe と idempotent

誤解が最も多い場所なので原文に当たります。RFC 9110 §9.2.1 は safe メソッドを「本質的に読み取り専用であり、サーバの状態を変更しない」と定義し、GET、HEAD、OPTIONS、TRACE を safe に分類します。§9.2.2 は idempotent を「複数回の同一リクエストがサーバに意図する効果が、一回のリクエストと同じである」と定義し、GET、HEAD、PUT、DELETE、OPTIONS、TRACE を idempotent に分類します。POST は冪等ではありません。

メソッドsafeidempotent
GETはいはい
HEADはいはい
OPTIONSはいはい
TRACEはいはい
PUTいいえはい
DELETEいいえはい
POSTいいえいいえ

3-1. 誤解 1 — 「PUT は更新、POST は作成」

仕様はそう言っていません。§9.3.4 は PUT を「対象リソースの状態が作成または置換される」よう要求するものと定義するので、PUT は作成もします。§9.3.3 の POST は「リソース自身の固有の意味論に従って表現を処理する」よう要求するものです。分かれ目は対象 URI が結果のリソースを指すかどうかです。クライアントが URI を決めるなら PUT、サーバが決めるなら POST です。

3-2. 誤解 2 — 「DELETE を二回呼んで 404 なら冪等ではない」

冪等の定義はサーバに対する意図した効果が同じということであって、応答が同じということではありません。一回目が 204、二回目が 404 でも「そのリソースが存在しない」という効果は同じなので冪等です。404 を成功と見るかどうかは別の契約です。

3-3. 誤解 3 — 「冪等なら再試行は安全」

冪等性はリクエストがサーバに届いたときの性質です。ネットワークのタイムアウトで応答を受け取れなければ、届いたかどうかすら分かりません。PUT なら再試行は安全ですが、POST なら重複作成が起きるので、POST には冪等性キーが必要です。

例 — 冪等性キーをヘッダで受け取る形。

POST /v1/payments
Idempotency-Key: 5f2a9c1e-6f6c-4a54-9f2f-27ab19d1b3c4
Content-Type: application/json

{ "orderId": "ord_01J9X", "amount": 15000, "currency": "KRW" }

キーの保存期間、キーが同じで本文が違うときの挙動(たいてい 409)、同じキーが同時に届いたときの挙動まで書いて契約が完成します。詳しくは 冪等性と再試行: 信頼できる API で扱います。

3-4. safe メソッドに副作用を入れない

GET で状態を変えると、プリフェッチやプロキシキャッシュ、クローラがその変更をランダムに引き起こします。要点は副作用の禁止ではなく、クライアントがその効果に責任を負わないことです。閲覧カウンタの増加は構いませんが、決済の承認は駄目です。


4. ステータスコードは契約である

ステータスコードはクライアントが分岐する値なので戻せません。RFC 9110 の定義で、よく取り違えられるものだけ整理します。

コードRFC 9110 の定義
400クライアント側の誤りにより、サーバが処理できないか処理しない
401「対象リソースに対する有効な認証資格情報がリクエストにない」
403「サーバはリクエストを理解したが、履行を拒否する」
404対象リソースの現在の表現が見つからない
409「リクエストが対象リソースの現在の状態と競合する」
422コンテンツタイプと構文は理解したが、指示を処理できない
500 / 503サーバ内部エラー / 一時的な過負荷または保守

4-1. 401 と 403 の境界

RFC 9110 は §15.5.2 で 401 を、§15.5.4 で 403 を定義します。401 は認証、403 は認可です。実務でよく出る第三の選択肢は、存在を隠すために 403 ではなく 404 を返すことです。これも契約なのでドキュメントに書きます。書かなければクライアントは 404 を「削除済み」と解釈してキャッシュを消します。

4-2. 400 と 422 の境界

仕様に沿えば、構文が壊れていれば 400、構文は正しいが値の意味が誤っていれば 422 です。どちらにせよ一つだけ選んで一貫して使います。混ぜるとクライアントは結局どちらも同じ分岐で処理するようになります。

4-3. 429 は RFC 9110 にない

429 Too Many Requests は RFC 6585 に定義されています。原文は「利用者が与えられた時間内に多すぎるリクエストを送った(レートリミット)」という意味だとし、応答に「どれだけ待つべきかを示す Retry-After ヘッダを含めてもよい(MAY)」と書きます。MAY なので、クライアントはこのヘッダがない可能性を前提にする必要があります。同じ RFC の 428 Precondition Required は「オリジンサーバがリクエストを条件付きにすることを要求する」コードで、更新の喪失(lost update)を防ぐためのものです。

4-4. アンチパターン — 200 にエラーを載せる

例 — こうするとステータスコードが契約から抜けます。

HTTP/1.1 200 OK
{ "success": false, "errorCode": "INSUFFICIENT_BALANCE", "message": "残高不足" }

この応答は、プロキシ・ゲートウェイ・監視・再試行ミドルウェアのすべてに「成功」と見えます。エラー率のダッシュボードは 0% を指し、自動再試行は動かず、サーキットブレーカも開きません。


5. エラー応答の形式 (RFC 9457)

RFC 9457 は RFC 7807 を置き換える文書で、HTTP API のエラー表現を application/problem+jsonapplication/problem+xml のメディアタイプで定義します。特別な理由がなければ形式を自作せず、これを使います。§3.1 が定義するメンバーは五つです。

  • type: URI 参照であり、問題タイプの主識別子です。ない場合の既定値は about:blank です。
  • status: 助言的(advisory)であり、実際の HTTP ステータスコードと一致している必要があります。
  • title: 人が読む要約。ローカライズを除けば「発生ごとに変わるべきではない(SHOULD NOT)」と仕様は言います。
  • detail: 今回の発生についての説明。「デバッグ情報を与えるよりも、クライアントが問題を正すのを助けることに焦点を当てるべき」です。
  • instance: 今回の発生を識別する URI。

§3.2 は拡張メンバーを認め、クライアントは「認識できない拡張を必ず無視しなければならない(MUST)」と規定します。だからエラー応答へのフィールド追加は破壊的変更ではありません。

例 — フィールド単位の検証エラーを拡張メンバーで運ぶ形。

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/validation-failed",
  "title": "Validation failed",
  "status": 422,
  "detail": "amount must be greater than 0",
  "instance": "/v1/payments/req_01J9XQ",
  "errors": [
    { "field": "amount", "code": "min_value", "min": 1 }
  ]
}

5-1. type URI の設計

type はクライアントが分岐する値なので戻せません。規則は三つで足ります。安定していること(ドメインが変わっても値は変えない)、参照解決できると望ましいこと(必須ではない)、粒度を処理方法に合わせること(別々に処理する理由のない二つのエラーに違う type を与えると、クライアントが両者をまとめるコードを書きます)。

5-2. 人が読む文とコードが読む値を分ける

titledetail は人用で、type と拡張メンバーはコード用です。この分離を守らないとクライアントは detail の文字列を正規表現で照合し始め、その瞬間にエラーメッセージが契約になり、文言を一つ直すのにクライアントのデプロイが必要になります。

§5 はセキュリティにも触れます。問題詳細に含める情報は「慎重に精査されなければならず」、「スタックダンプのような実装の詳細」を露出しないよう明示しています。


6. ページネーション: offset と cursor

6-1. 二方式の実際の違い

項目offset ベースcursor ベース
任意ページへのジャンプ可能不可能
全件数の表示容易高価または概算
深いページのコスト深さに比例して増加深さと無関係
挿入・削除中の一貫性重複と抜けが発生安定
契約が固まる地点ページ番号カーソル文字列

offset の問題は性能だけではありません。1 ページ目を見ている間に新しい項目が前に挿入されると 2 ページ目で同じ項目をもう一度見ることになり、削除されると項目が丸ごと飛ばされます。一覧がよく変わる API で、これはバグ報告になって返ってきます。

6-2. カーソルは不透明でなければならない

例 — レスポンス封筒と不透明カーソル。

{
  "data": [
    { "id": "ord_01J9XQ", "createdAt": "2026-08-15T09:30:00Z" }
  ],
  "nextCursor": "eyJjIjoiMjAyNi0wOC0xNVQwOTozMDowMFoiLCJpIjoib3JkXzAxSjlYUSJ9",
  "hasMore": true
}

カーソルの内部構造をドキュメントに書くとクライアントがデコードして操作し始め、そうなると構造を変えられません。カーソルはサーバが渡した値をそのまま返すトークンとだけ規定すれば、あとでソートキーを変えても署名を足しても壊れません。

6-3. ソートキーが一意でないとカーソルは壊れる

createdAt だけでカーソルを作ると、同じミリ秒の項目で重複か抜けが起きます。ソートキーには必ず一意な副キーを足します。上の例のカーソルが時刻と識別子を一緒に持っている理由です。

6-4. 戻せないのはレスポンス封筒

data / nextCursor / hasMore という封筒構造はあとから変えられません。配列をトップレベルで返してしまうと、メタデータが必要になった瞬間に応答の形全体を変えることになり、それは破壊的変更です。


7. 時刻・金額・列挙の表現

三つともパースコードと保存スキーマにそのまま刻まれるので戻せません。

例 — この節の推奨表現。

{
  "createdAt": "2026-08-15T09:30:00Z",
  "scheduledAt": "2026-09-01T14:00:00+09:00",
  "scheduleTimeZone": "Asia/Seoul",
  "amount": 15000,
  "currency": "KRW",
  "status": "partially_refunded",
  "canceledAt": null,
  "externalId": "9007199254740993"
}

7-1. 時刻

  • 文字列で送ります。 ISO 8601 拡張形式(例: 2026-08-15T09:30:00Z)が安全です。数値の epoch は秒とミリ秒を区別できず、誤ってパースすると 1970 年か五万年後になります。
  • オフセットを必ず含めます。 オフセットのない文字列はサーバのタイムゾーンを知らないと解釈できず、そのタイムゾーンはインフラの都合で変わります。
  • 未来の約束にはタイムゾーン名が要ります。 オフセットはその時点の規則を固定しますが、サマータイムや国の政策は変わります。「9 月 1 日午後 2 時ソウル」はタイムゾーン名で保存する必要があります。

7-2. 金額

  • 整数の最小単位と通貨コードを一緒に送ります。 浮動小数は使いません。0.1 + 0.2 が 0.3 でない問題を精算で見つけると、原因追跡に何日もかかります。
  • 小数桁数は通貨ごとに違います。 ウォンは 0 桁、ドルは 2 桁、一部は 3 桁です。「セント単位の整数」は通貨コードなしに成立しないので、ISO 4217 コードを一緒に送ります。
  • 為替が絡むなら換算時点と為替値も入れます。 あとから入れると過去データに値がなく、二重ロジックが生まれます。

7-3. 列挙

新しい列挙値の追加が破壊的かどうかは、クライアント側の扱いの規約次第です。すべての値を漏れなく分岐するコードを書いているなら破壊的で、未知の値の既定動作が定義されているなら破壊的ではありません。だから v1 のドキュメントにこの一文を入れます。

  • 「このフィールドには新しい値が追加されます。未知の値は unknown として扱い、元の文字列を保存して送り返してください。」

値の名前は小文字とアンダースコアに固定します。大文字小文字の混在は言語ごとに違う正規化を受け、比較のバグを生みます。

7-4. null とフィールドの不在と空の値

三つの状態を区別するかどうかを今決めます。「キャンセルされていない」を canceledAt: null と見るかフィールドの不在と見るかで部分更新のロジックが変わり、部分更新 API があるなら「消せ」と「触るな」を区別する方法が必要になります。

7-5. 大きな整数は文字列で

JSON の数値は多くの言語で倍精度浮動小数としてパースされます。安全な整数範囲を超える識別子を数値で送ると、末尾の桁が静かに変わります。上の例の externalId を文字列にした理由です。


8. 変更とバージョニング — 拡張・移行・縮小

8-1. Parallel Change

マーチン・ファウラーがまとめた Parallel Change は、インターフェースの変更を三つに分けます。拡張の段階では「インターフェースを拡張して旧版と新版の両方を支える」、移行の段階では「旧版を使うすべてのクライアントを新版へ更新する」とし、この作業は「漸進的に行える」と書きます。そして「すべての利用箇所が新版へ移行したら、縮小の段階を行って旧版を削除する」とします。ファウラーはこのパターンの出典をジョシュア・ケリエフスキーとしています。

拡張(expand)   新旧どちらも支える   ── サーバのデプロイだけで可能
移行(migrate)  クライアントを一つずつ移す ── 使用量の計測が必須
縮小(contract) 旧版を削除          ── 使用量ゼロを確認してから

核心は、移行段階の長さをサーバが制御できないと認めることです。縮小の時点はカレンダーではなく使用量が決めます。だから拡張の段階に必ず併せて入れるべきなのが旧版の使用量計測です。

8-2. 何が破壊的変更か

破壊的なもの: レスポンスのフィールド削除・改名・型変更、リクエストへの必須フィールド追加、ステータスコードやエラー識別子の変更、既定値と既定ソートの変更、レートリミットの引き下げ、そしてクライアントがすべての値を分岐する場合に限り列挙への新しい値の追加。

破壊的でないもの: レスポンスとリクエストへの任意フィールド追加(無視の規約がドキュメントにあるとき)、新しいエンドポイントの追加、エラー応答への拡張メンバー追加、性能改善。

境界にあるものが事故を起こします。「任意フィールドの追加」が安全なのはクライアントが未知のフィールドを無視するときだけで、厳格なデシリアライズを使う側にはフィールド追加も破壊的です。だから 1-3 節の三文が必要になります。

8-3. 論争点 — パスのバージョニングとヘッダ・メディアタイプのバージョニング

勝者のいない論争です。軸だけ整理します。

  • 可視性とデバッグ: パスにバージョンがあればログとアドレスバーですぐ見え、ヘッダ方式はリクエストを開かないと分かりません。
  • ルーティングとキャッシュ: パス方式はルーティングとキャッシュキーの分離が自明です。ヘッダ方式は Vary の扱いを正確にする必要があり、中間キャッシュが誤ると交差汚染が起きます。
  • 粒度とバージョン爆発: パス方式は API 全体を一度に上げるためバージョンが大きく飛びます。メディアタイプ方式はリソース単位で分けられますが、維持対象が増えます。
  • クライアントの利便性: ブラウザから URL だけで呼ぶ利用者が多いならパス方式が楽です。

どちらにせよ、バージョンを上げずに解決できるかを先に確認するほうが実益が大きいです。バージョン番号が速く上がる API は、たいてい拡張の段階を飛ばしています。

8-4. 廃止の手順

  • 廃止をレスポンスヘッダで知らせます。ドキュメントだけでは誰も読みません。予定時期と代替の経路も併せて知らせます。
  • 使用量をクライアント単位で計測します。総量だけでは誰に連絡すべきか分かりません。
  • 縮小の前に短い遮断リハーサルをします。旧版を一時的に 410 で返してから戻せば、残っている利用者が現れます。

段階的な置き換えの大きな絵は Strangler Fig パターン完全ガイド で扱います。


9. 契約を検証する方法

ドキュメントに書いた契約と実際に出ていく応答が違うなら、契約はドキュメントではなく応答のほうです。

9-1. スキーマを単一のソースにする

OpenAPI スキーマをコードから生成するか逆かはチームごとに違います。大事なのはどちらか一方が単一のソースであることです。両方を手で管理すれば必ずずれ、そのずれはクライアントが先に見つけます。

9-2. 破壊的変更を CI で止める

例 — スキーマ差分をゲートにする流れ。

PR が開かれる
  └─ 直前のコミットのスキーマと現在のスキーマを比較
       ├─ フィールド削除 / 型変更 / 必須追加  → 失敗、レビュアー承認が必要
       ├─ 任意フィールド追加 / 新エンドポイント → 通過
       └─ エラー type URI の変更              → 失敗

このゲートの値打ちは止めることより表に出すことにあります。破壊的変更が必要なときは人が承認すればよく、問題は破壊的と知らずにマージされる場合です。

9-3. コンシューマ駆動契約テスト

利用者が「私はこのフィールドをこう使う」という期待を契約として登録し、提供側のパイプラインがその期待を検証します。本当の利得はテストではなく、誰が何に依存しているかが一覧として残ることであり、その一覧が 8-4 節の廃止手順での連絡先になります。

9-4. ドキュメントの例をスキーマで検証する

手で書いた例の応答は最も速く古びます。例をスキーマ検証の対象に入れれば、ドキュメントは自動的に最新に保たれます。

9-5. 実トラフィックのリグレッション

本番トラフィックの標本を新旧の版に同時に流して応答の差を比べます。値の分布の変化、並び順の変化、空配列と null の入れ替わりのように、スキーマ検証が拾えないものを拾います。応答を手で組んで確かめるときは HTTP Request BuilderHTTP Status Codes が役に立ちます。


クイズ: 理解度を確認しましょう

クイズ 1: 一覧 API がトップレベルで配列を返しています。ここに総件数を足してほしいと要望が来ました。何が問題でしょうか?

答え: トップレベルの配列にはメタデータを付ける場所がないため、応答全体をオブジェクトに変える必要があり、これは破壊的変更です。

解説: 一覧応答の封筒構造は戻せない決定です。最初からオブジェクトで包んでおけば、あとからフィールドを足すのは任意フィールドの追加にすぎず安全です。すでに配列で出ているなら、拡張・移行・縮小を踏む必要があります。

クイズ 2: DELETE を二回呼んだら一回目は 204、二回目は 404 でした。この API は冪等性に反していますか?

答え: いいえ。RFC 9110 の冪等の定義はサーバに意図した効果が同じということであって、応答が同じということではありません。

解説: §9.2.2 は冪等性を「複数回の同一リクエストがサーバに意図する効果が、一回のリクエストと同じである」と定義します。二回呼んだあとの状態は「そのリソースが存在しない」で同じなので冪等です。ただし再試行ロジックが 404 を成功と見るかは別の契約であり、書いておかないと再試行中に出た 404 がエラーとして集計されダッシュボードが歪みます。

クイズ 3: 決済 API が残高不足のときに HTTP 200 でエラー本文を返しています。アプリは正しく動いていますが、何が壊れていますか?

答え: 経路上のすべての中間層がこの応答を成功と読みます。エラー率の指標、自動再試行、サーキットブレーカ、ゲートウェイのポリシーがすべて無力化されます。

解説: ステータスコードはアプリだけが読む値ではなく、プロキシ、ロードバランサ、観測パイプライン、クライアントライブラリがすべてこの値で動きます。残高不足はクライアントが直せるリクエストのエラーなので 4xx が正しく、本文は RFC 9457 の problem details に載せてエラー種別を type URI で識別させます。

クイズ 4: レスポンスの status 列挙に値を一つ追加しようとしています。破壊的変更かどうかをどう判断しますか?

答え: 未知の値に出会ったときの扱いの規約がドキュメントにあるかで判断します。なければ破壊的です。

解説: 列挙値の追加自体は中立で、破壊性は利用者側の規約が決めます。すべての値を漏れなく分岐するクライアントにとって、新しい値はそのまま実行時エラーです。v1 のドキュメントに扱いの規約を先に書くほうが、v2 を作るより圧倒的に安上がりです。

クイズ 5: 旧版のエンドポイントを消したいのですが、使用量がゼロか確信が持てません。まず何をすべきでしょうか?

答え: クライアント単位の使用量計測を先に付け、そのあと短い遮断リハーサルをします。

解説: Parallel Change の縮小段階は「すべての利用箇所が新版へ移行したあと」にだけ行います。総量だけでは残る利用者が誰か分からないので連絡できず、計測なしに削除すれば障害として発見することになります。


おわりに

API 設計で時間を使うべきなのは難しい問題ではなく、戻しにくい問題です。二つはよくずれます。キャッシュの無効化は難しいけれど戻せて、識別子の形式は易しいけれど戻せません。

最初のリリース前に確定する五つは、リソース境界、識別子、メソッド意味論、ステータスコード体系、エラー形式です。ここに 1-3 節の三文を入れておけば、あとで必要になる変更のかなりの部分が破壊的変更から拡張へ下ります。変えられるものに完璧を求めて、変えられないものを雑に決める。それだけ避ければ、API 設計の半分は終わりです。


参考資料

  • RFC 9110 — HTTP Semantics — §9.2.1 の safe 定義と対象メソッド、§9.2.2 の idempotent 定義と POST が冪等でないこと、§9.3.3 POST と §9.3.4 PUT の定義、400・401・403・404・409・422・500・503 の定義、§15.5.2 と §15.5.4 による 401(認証)と 403(認可)の区別を引用しました。2026-08-15 確認。
  • RFC 6585 — Additional HTTP Status Codes — 429 の定義と Retry-After ヘッダが MAY であること、428 Precondition Required が更新の喪失を防ぐためのものであること、429 が RFC 9110 ではなくこの文書にあるという事実を引用しました。2026-08-15 確認。
  • RFC 9457 — Problem Details for HTTP APIs — RFC 7807 を置き換えること、application/problem+json メディアタイプ、§3.1 の五つのメンバー定義、§3.2 の拡張メンバーとクライアントの無視義務、§5 のスタックダンプ露出の禁止を引用しました。2026-08-15 確認。
  • Parallel Change — Martin Fowler — 拡張・移行・縮小の三段階の原文の説明、移行が漸進的に行えること、このパターンがジョシュア・ケリエフスキーのものであるという出典を引用しました。2026-08-15 確認。
  • 戻すコストの判別質問四つ、決定別の戻すコスト表、識別子三方式の比較表、最初のドキュメントに入れる三文、縮小前の遮断リハーサル、スキーマ差分ゲートの分類は、上記資料にそのまま出てくるものではなく、この記事で整理した手順です。

関連記事

完全ガイドシリーズ