Part 1 から Part 3 で決めた設計を、仕様書としてどう書くかを扱う。1 リソース分の仕様書を抜粋しながら進める。
仕様書のフォーマットは Stripe の API リファレンスを下敷きにし、個々の判断は Claude Code と議論しながら決めた。
前提
この仕様書は、次の流れの中にある。
- API 仕様書を Markdown で書く
- 仕様書をエージェントに渡して実装させる
- 実装は NestJS で、Swagger のデコレーターをソースコードに書く。Swagger ドキュメントはそこから生成する
仕様書が唯一の正である。 仕様書のレビューには時間がかかるため、重複なく正確に書くことが重要になる。エージェントが読む入力でもあり、実装もドキュメントもそこから派生する。仕様書をどう分割し、どの情報をどこに置くかは、読みやすさだけの問題ではなく、エージェントの実装の質にも影響する。
仕様書は 1 リソースに 1 つ
以前は 1 つの API につき 1 つの仕様書を書いていた。今は 1 つのリソースに関する API 群を 1 つの仕様書にまとめる。
- リソースのライフサイクルを 1 つの仕様書で追える。作成から削除までを見た上で設計できる
- 同じリソースを扱う API 群はリクエストとレスポンスの定義が重複する。1 ファイルにまとめると重複を減らせる
ファイル名は <リソース名>-api.spec.md とし、リソース名は URL の複数形リソースに一致させる(customers-api.spec.md)。
仕様書の構成
カスタマーの API を例にする仕様書の構成。
# カスタマー API 仕様書
## 変更履歴
## 概要
## カスタマーオブジェクト
## カスタマーを作成する
### パラメーター
### 戻り値
## カスタマーを更新する
### パラメーター
### 戻り値
## カスタマーを取得する
### パラメーター
### 戻り値
## カスタマー一覧を取得する
### パラメーター
### 戻り値
## カスタマーを削除する
### パラメーター
### 戻り値
## エラー
オブジェクトの章を先に置き、その後にエンドポイントを並べる。各エンドポイントの節は、URL 行、curl 例、概要文、パラメーター、戻り値の順で書く。
オブジェクトの章にフィールドの制限を集める
## カスタマーオブジェクト
```json
{
"id": 12345,
"object": "customer",
"name": "山田太郎",
"email": "yamada@example.com",
"address": { "city": "渋谷区", "prefecture": "東京都" },
"status": "active",
"created_at": "2026-01-15T10:30:00.000Z"
}
```
### email
`nullable string`
カスタマーのメールアドレス。RFC 5321 準拠、最大 254 文字。未設定の場合は `null`。
### status
`enum of string`
カスタマーのステータス。以下のいずれかの文字列。
- `"active"`: 有効
- `"inactive"`: 無効
### address
`nullable object`
カスタマーの住所。未設定の場合は `null`。
#### address.city
`string`
市区町村。最大 50 文字。
#### address.prefecture
`string`
都道府県。47 都道府県名のいずれか。
オブジェクトのプロパティごとに、説明と共に、型・nullable・フォーマット・最大長・列挙値などの制限事項を記載する。そのフィールドが取りうる値の範囲は、この章だけを読めば分かる状態にする。
ネストしたフィールドは address.city のようにドット記法で見出しに書く。
型の書き方
- 基本型:
string/integer/number/boolean/object/array。小数を取りうる値はnumber、整数に限る値はintegerと書き分ける - null 許容:
nullable stringのようにnullableを前置する - 配列:
array of integerのように要素型を明記する - 列挙値:
enum of stringのように基になる型を明記し、取りうる値と意味を並べる。数値のコードならenum of integer
パラメーターは 1 つの節にまとめる
## カスタマーを作成する
`POST /v1/customers`
```bash
curl -X POST "${BASE_URL}/v1/customers" \
-H 'Content-Type: application/json' \
-d '{ "name": "山田太郎", "email": "yamada@example.com" }'
```
新しいカスタマーを作成する。
### パラメーター
#### name
`string` 必須
カスタマーの氏名。
#### email
`string` 任意
カスタマーのメールアドレス。
### 戻り値
カスタマーオブジェクトを返す。
パス・クエリ・リクエストボディを区別せず、すべて パラメーター の 1 節に並べる。HTTP メソッドで文脈が決まるためである。GET のパラメーターはクエリ、POST のパラメーターはリクエストボディに決まる。パスパラメーターだけは説明文に「パスパラメーター」と明記する。
種別ごとに節を分ける方式も正しい。OpenAPI が in: path / in: query を持つように、機械可読な定義に近い形になる。代わりに読み手は 3 つの節を往復する。
作成と更新でパラメーターが重複しても、それぞれの節に独立して書く。作成には必須があり、更新はすべて任意になるなど、文脈が異なるためである。
オブジェクトの章と分担する
パラメーターの章には、入力時にだけ意味を持つ情報を書く。必須か任意か、デフォルト値がいくつか、といった情報である。オブジェクトの章に書いた型や最大長は繰り返さない。
こう分けると、それぞれの情報は 1 か所にだけ載る。
- 書き込めないフィールドはオブジェクトの章にだけ現れる
- 書き込めるフィールドは、値の制限をオブジェクトの章に、入力時の扱いをパラメーターの章に置く
両方に書く方式も、1 つの節を読めば分かる点で読みやすい。代わりに同じ制限が 2 か所に載り、片方だけが更新される問題がある。
列挙値はオブジェクトの章へリンクする
作成・更新のパラメーターがオブジェクトのプロパティと同じ値を取る場合は、値を並べずにオブジェクトの章へリンクする。
#### status
`enum of string` 任意
カスタマーのステータス。[`status`](#status) と同じ値を取る。
リンクは「同じ型である」ことを示す。 値が 2 か所に並んでいると、同じなのが意図か偶然かが読み取れない。
値の集合が異なるパラメーターは、取りうる値を並べる。絞り込みを解除する "all" を含むフィルターや、一部の値だけを受け付けるパラメーターである。
制約はバリデーションに必要な分だけ書く
全仕様書で共通の前提は、個別のフィールドに繰り返さない。
- 必須フィールドは
nullを受け付けない - 文字列フィールドは、任意でも空文字列と空白だけの文字列を受け付けない。欠損を表す入力を、省略と
nullに限るためである - 任意フィールドは省略できる。作成 API では
nullの送信も省略と同じに扱う - 更新 API で任意フィールドを省略した場合、その値は変更されない
- 更新 API では、
nullableと明記したフィールドだけがnullを受け付ける。nullでその値をクリアする
個別に書くのは、文字列長・数値の範囲・列挙値・フォーマット・配列の最大要素数・一意性である。このうち文字列の最大長はすべての文字列フィールドに明記する。 暗黙のデフォルトを設けず、次の優先順で決める。
- 外部標準(メールアドレスは RFC 5321 の 254、URL は 2048)
- 業務・UX 上の現実的な最大値(画面の表示幅、入力欄の制約)
- 悪用と DoS を防ぐ上限。自由記述欄でも 5000〜10000 などの明示的な天井を置く
- DB のカラム長は根拠にしない。カラム長を超える値は保存できないので実際の上限としては効くが、業務上必要な長さがそれを超えるなら、変えるのはカラムのほうである
クロスフィールドの制約は、どちらか片方のパラメーターにだけ書く。両側に書くと更新時にずれる。
実装の詳細を書かない
仕様書は API 消費者に向けたドキュメントである。外から観測できない実装の詳細は書かない。
- テーブル名、カラム名、外部キー、DB 制約、DB 仕様書へのリンク
- 環境変数とその既定値、設定ファイル、内部のクラスやモジュールの構成
- ログ出力や監視の挙動
# 避ける
`customers` テーブルの `email` カラムに UNIQUE 制約があるため重複できない。
# こうする
既に登録されているメールアドレスは指定できない。
一意性や必須性は、DB の事情ではなく API が受け付ける値の条件として書く。 DB のスキーマが変わっても API の振る舞いが変わらないなら、仕様書を直す必要はない。
変更履歴の書き方
変更履歴は必ず書く。 API の変更の軌跡を追えるだけでなく、実装済みと実装待ちを区別できる。仕様書を先に更新してエージェントに実装させるワークフローでは、この区別がそのまま次の作業の入口になる。
## 変更履歴
- **20xx-xx-xx**: メールアドレスの重複チェックを追加(実装待ち)
- **2026-04-15**: 初版作成
- エントリは日付の降順に並べる
- 未実装の変更は
20xx-xx-xxの 1 エントリにまとめ、概要の末尾に「(実装待ち)」を付ける。リリース時に日付を実際の日付に置き換え、注記を削除する - 初回のエントリは「初版作成」と書く。「更新」「変更」は使わない
仕様書に書かないもの
-
HTTP ステータスコード: 「戻り値」の節には返却されるオブジェクトの内容だけを書く。消費者は
2xxを成功、4xxと5xxをエラーとして扱えばよい - 認証の仕組み: 共通ドキュメントに一元記述し、個別の仕様書で繰り返さない。特定のロールや権限が必要な場合だけ「概要」に書く
- 独自の見出し: 「取得可能条件」「削除できる条件」「競合の扱い」のような見出しを足さない。該当する内容はエンドポイントの概要文に自然文で書く
非推奨のエンドポイント
新旧 2 つのエンドポイントが同時に存在する場合、旧エンドポイントにも新エンドポイントと同じ独立した節を設け、挙動を完全に記述する。「挙動は新エンドポイントと同じである」と参照だけで済ませない。旧エンドポイントの節だけを読んで挙動が分かる状態にする。
非推奨であることは節の先頭、URL 行と curl 例より前に書く。見出しにも「旧 API・非推奨」を含める。
これで 4 回のシリーズを終える。