はじめに(この記事でわかること)
API設計って、CRUDのエンドポイントを生やす作業だと思われがちですが、実務で痛い目を見るのは「仕様の揺れ」や「例外の扱い」「認可の穴」みたいな“設計の前提”が曖昧なまま進んだときです。
この記事では、私がAPIを作り始める前に 最初に固定していること をまとめます。
背景・前提条件
- チーム開発(レビューあり・複数人が触る)
- フロント/外部連携がある(= 契約としてのAPIになる)
- 後から機能追加・改修が入る前提(= 初期の「楽」は後で高くつく)
課題・問題点
API設計の初手で曖昧だと、後からこうなりがちです。
- エラー形式がバラバラでフロントが例外処理地獄
- 認可の粒度が合わず、画面権限制御が崩れる
- 更新系の「整合性」や「同時更新」が事故る
- 仕様変更のたびにURLやレスポンスが揺れて、利用側が疲弊する
検討した選択肢
A. とりあえずCRUDを作ってから考える
- 初速は出る
- ただし後から「全部直す」が発生しやすい(契約を変えるのは辛い)
B. 最初に“契約のコア”だけ決める
- 最初は少し遅い
- でも後からの変更が局所化しやすい(実務ではこっちが勝つことが多い印象)
このブログではBを推します。理由はシンプルで、APIは利用側が増えるほど変更コストが指数的に増えるからです。
採用した設計・実装(最初に決める項目)
1) リソースの境界(何が主語か)
- 「何を1つのリソースとして扱うか」を先に定義します
例:reservationsの中にparticipantsを持たせるのか、別リソースにするのか、など。
ここが曖昧だと、URLもレスポンスも泥団子になります。
2) 命名規則(URL / フィールド / ステータス)
- URLは複数形で統一するか(
/events) - フィールドは
snake_case/camelCaseどちらか - 列挙値の表現(
status: "draft" | "published"など)
「好み」っぽく見えるところですが、統一が壊れると利用側の認知負荷が増えます。
3) ページング・ソート・フィルタの基本形
CRUDだけ作っても、実務はすぐ「一覧」が重くなります。
page/per_pageか、offset/limitかsort=created_at:descのように複合ソートを許可するか- フィルタの表現(
?status=published&category=backend)
一覧APIの“型”を先に揃えると、後からの追加が楽になります。
4) エラーの形式(フロントが一番困るところ)
最低限、これだけは固定します。
code(機械判定用)message(人間向け)details(入力エラーのフィールド単位)
「毎回HTTPステータスだけ見ればいい」は現場では崩れやすいので、アプリケーションエラーの規約を持つのが安全です。
5) 認証・認可の入口(どこで担保するか)
- 認証:誰か(トークン/セッション)
- 認可:何ができるか(画面権限・ロール・スコープ)
API設計の最初にここを決めないと、後から「このエンドポイントだけ例外」が増えます。
私は「依存(ミドルウェア/DI)で必ず通す」など、通過点を固定するようにします。
6) 変更系の設計(PUT/PATCH/部分更新/状態遷移)
更新系は事故ポイントです。
PUT(全置換)かPATCH(部分更新)か- 状態遷移は「更新」でやるか「アクションAPI」にするか
ここはCRUDの延長に見えて、後から必ず揉めます。
7) 冪等性と同時更新(競合が起きる前提)
- 冪等キー(決済/予約などで二重実行が致命的な時)
- 楽観ロック(
version/updated_atによる競合検知)
「後で入れよう」はたいてい後で入れられません(契約が変わるので)。
実装例(雰囲気)
エラー形式を固定するだけでも、利用側はかなり楽になります。
- 入力エラーは
detailsにフィールド単位で詰める - ドメインエラーは
codeで分類する
実務での注意点・落とし穴
- 最初に決めるのは「全部」じゃなくていい
ただし 契約が揺れるところ(一覧/エラー/認可/更新) は先に固定したいです。 - “使う側”の実装コストを観察する
APIは作って終わりではなく、使われて初めて完成します。
まとめ
API設計の初手はCRUDよりも、揺れやすい契約(一覧・エラー・認可・更新)を固定するのが効きます。
ここが固まると、後から機能が増えても「同じ型で増やす」だけになります。