リソース設計 — 一度公開したら変えられない契約
URL は動詞ではなく名詞で切る、の理由。そして「きれいな REST」に収まらない操作をどう扱うか。
先に読んでおくとよい記事
この記事の進み方
What — リソースをどう切るか
REST の中心的な考え方は、操作ではなくモノに名前を付けることです。
操作ごとに URL を作る。増え続け、統一感がなくなる。
POST /getUser
POST /getUserList
POST /createUser
POST /updateUser
POST /updateUserEmail ← 部分更新のたびに増える
POST /deleteUser
POST /searchUsersByName
POST /searchUsersByEmail ← 条件ごとに増える
→ 命名規則が人によってぶれる
→ キャッシュもできない(全部 POST)- URL の数
- 操作の数だけ増える
- 命名の一貫性
- 保ちにくい
- キャッシュ
- 効かせにくい
- 学習コスト
- 一覧を読む必要がある
- –ただし、複雑な業務操作には向いていることもある。後述。
動詞で切ると、操作が増えるたびに URL が増える。名詞で切ると、URL の数は概念の数に収まり、操作はメソッドで表現される。
HTTP メソッドの性質
- GET
- 取得。安全(状態を変えない)で冪等。キャッシュできる。GET で状態を変えてはいけない。
- POST
- 作成・任意の処理。冪等ではない。2回送ると2件できる。
- PUT
- 指定した内容で全体を置き換える。冪等(何回送っても同じ状態になる)。
- PATCH
- 一部だけを更新する。冪等とは限らない(差分の種類による)。
- DELETE
- 削除。冪等(2回目は「すでに無い」だが、結果として状態は同じ)。
- 安全(safe)
- サーバーの状態を変えない。GET / HEAD / OPTIONS。
- 冪等(idempotent)
- 何回実行しても結果の状態が同じ。GET / PUT / DELETE。
GET で状態を変えてはいけない理由は3つあります。
- ブラウザやプロキシが勝手に先読みすることがある
- キャッシュされるので、2回目が実行されない
<img src="/delete?id=1">のような形で CSRF が成立する
CRUD に収まらない操作
実務では、「注文を確定する」「パスワードをリセットする」のような、CRUD に収まらない操作が必ず出てきます。
❌ POST /confirmOrder 動詞の URL が増えていく
❌ PATCH /orders/123 { status: "confirmed" } ← 状態を直接書かせる
✅ POST /orders/123/confirmation 状態遷移を「モノ」として表す
✅ POST /orders/123/cancellation
3つ目の形は、「確定」という行為の記録をリソースとして作るという発想です。実際、確定日時や確定者を記録したいことが多いので、モノとして扱うのが自然です。
2つ目(PATCH で status を直接書かせる)を避ける理由は、クライアントに状態遷移のルールを知らせてしまうためです。status: "shipped" を送れば発送済みにできる、という設計だと、サーバー側でルールを守りにくくなります。
レスポンスのフィールドを1つ削除するのは、どういう変更ですか?
Why — なぜ API は契約なのか
API とライブラリの内部関数の決定的な違いは、使い手を把握できるかです。
内部関数なら、リポジトリ全体を grep すれば呼び出し元が全部見つかります。シグネチャを変えても、コンパイルエラーで漏れが分かります。
API は違います。
- クライアントは別のリポジトリ、別の会社、別の言語で動いている
- モバイルアプリは古いバージョンが端末に残り続ける
- バッチやスクリプトは、動いている間は誰も存在を思い出さない
変更したときに壊れる場所を、変更する側が把握できません。 だから API の変更は、内部リファクタリングとはまったく違う重さを持ちます。
後方互換を保つ変更・壊す変更
| 変更 | 互換性 | 理由 |
|---|---|---|
| レスポンスにフィールドを追加 | ✅ 保たれる | 知らないフィールドは無視される |
| リクエストに任意のパラメータを追加 | ✅ 保たれる | 既存の呼び出しはそのまま動く |
| 新しいエンドポイントを追加 | ✅ 保たれる | 既存に影響しない |
| レスポンスからフィールドを削除 | ❌ 壊れる | 冒頭の事故 |
| フィールドの型を変える | ❌ 壊れる | パースに失敗する |
| 必須パラメータを追加 | ❌ 壊れる | 既存の呼び出しが 400 になる |
| エラー時のステータスコードを変える | ❌ 壊れる | 分岐が変わる |
| 列挙値を追加 | ⚠️ 場合による | クライアントが未知の値を扱えるか次第 |
追加は安全、削除と変更は危険というのが基本形です。
バージョニングの方法
① URL パス /v1/users /v2/users
② ヘッダ Accept: application/vnd.example.v2+json
③ クエリ /users?version=2
実務では①が最も多いです。理由は単純で、
- ブラウザで URL を開くだけで試せる
- ログを見れば、どのバージョンが使われているか一目で分かる
- ルーティングで分離しやすい
②のほうが「REST らしい」とされますが、デバッグのしやすさで①が選ばれます。
ただし、バージョンを増やすと保守対象が増えます。v1 と v2 を並行して動かすなら、バグ修正も両方に入れる必要がある。可能な限り、互換性を保つ変更で乗り切るほうが安上がりです。
「記事を公開する」操作を API にするとき、どれが扱いやすいですか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
この機能群の API をどう設計しますか?
- 社内の稟議申請システムに API を追加する
- 必要な操作: 申請の作成・一覧・詳細取得・下書き保存・申請提出・承認・却下・差し戻し・取り下げ
- 承認は複数段階(課長 → 部長 → 役員)で、誰がいつ承認したかを記録する必要がある
- 却下と差し戻しには理由のコメントが必須
- 外部の会計システムからも呼ばれる(先方のリリースサイクルは半年に1回)
- 将来、申請種別ごとの承認フローのカスタマイズを検討している
- · どの操作が「モノ」として表現できるか
- · 承認履歴を記録する必要があることは、設計にどう効くか
- · 半年に1回しかリリースしない相手がいることは、何を意味するか
「使われていないフィールドなので消した」という変更を、なぜ危険かを説明してください
- 社内のコードには参照が無かった
- デプロイ後に外部から3件の連絡が来た
- 今後の進め方まで決めたい
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。