REST API設計でURLとステータスコードを先に決める:エンドポイント命名と冪等性
REST APIのURL設計・HTTPメソッドとステータスコードの使い分け・ページング・バージョニング・冪等性を、実装前に決める手順とコピペで動くコードで。
「注文を作るAPI、いい感じに作っといて」
そうClaude Codeに頼んだら、5分で動くコードが出てきました。エンドポイントは POST /createOrder、成功すると 200 OK、エラーは全部 { "success": false }。動くは動く。でも翌週、フロント担当から「作成成功なのに200なんですか?201じゃ?」、モバイル担当から「エラーのとき何を見れば?」と詰められて、僕は黙りました。
そのとき気づいたんです。遅いのはコードを書く手じゃなくて、約束を決める頭のほうだと。Claude Codeは速い。速いからこそ、URLの形もステータスコードも曖昧なまま量産されて、あとから直すのがいちばん高くつく。
この記事は、実装に入る前にREST APIの「約束」を固める話です。リソース指向のURL、メソッドとステータスコードの対応、ページング、バージョニング、そして冪等性。コードを1行も書く前に決めておくべきことだけを、僕がやらかした失敗とセットで書きます。
この記事の要点
- URLは「名詞(リソース)」、操作は「HTTPメソッド」。
POST /createOrderではなくPOST /ordersに寄せる。 - ステータスコードは成功も失敗も先に表で固定する。
200 OKでsuccess: falseを返すのが最悪のパターン。 - 一覧は最初からページングを決める。
page=2ではなくカーソル(limit+after)と固定の並び順。 - 破壊的変更だけ
/v2に分ける。任意フィールドの追加でバージョンは上げない。 POSTの再送で二重登録しないよう、Idempotency-Keyを設計段階で入れる(あとから足すと地獄)。
REST自体は「Representational State Transfer」の略ですが、ここでは難しく考えず、注文・ユーザー・請求書みたいな対象(リソース)をHTTPで操作するときの決めごと、くらいに捉えてください。リソースは扱う対象、メソッドは操作、ステータスコードは結果の合図です。
公式の根拠は、メソッドとステータスコードがMDNのHTTP request methodsとHTTP response status codes、仕様書はOpenAPI Specificationを見ます。2026年6月時点でOpenAPIのlatestは3.2.0ですが、この記事の例はリンターや生成ツールで扱いやすい openapi: 3.1.0 にしています。実装の足回りはClaude CodeでREST API実装、スキーマ定義のやり方はOpenAPIとSwagger入門、GraphQLと迷っている人はGraphQLとRESTの住み分けを合わせて読んでください。
まず設計の言葉をそろえる
僕が最初にハマったのは、チーム内で言葉の意味がズレていたことでした。「エンドポイント作っといて」と言ったのに、相手は「リソースを1個生やす」と解釈して、URLの形が人によってバラバラになった。だから依頼の前に、最低限この6語だけは同じ意味に揃えます。
| 用語 | やさしい説明 | この記事での例 |
|---|---|---|
| リソース | APIで扱う名詞。作る・取る・直す・消す対象 | orders, customers |
| エンドポイント | リソースに入るURLとメソッドの組 | GET /v1/orders |
| メソッド | 「何をしたいか」を表すHTTPの動詞 | GET, POST, PATCH |
| ステータスコード | 成功・入力ミス・未認証などを数値で返す合図 | 200, 201, 400, 404 |
| ページング | 一覧を小分けにして返す仕組み | limit+after |
| 冪等性(べきとうせい) | 同じ操作を何度送っても最終結果が同じ性質 | 同じキーの注文作成は1回だけ |
冪等性は耳慣れない言葉ですが、要は「再送しても壊れない」性質のこと。エレベーターのボタンを5回押しても1回しか来ないのと同じです。Claude Codeは文脈を読むのが上手ですが、この設計語を勝手に正しく補完してくれる保証はありません。言葉と方針を先に渡すのが、結局いちばん速い。
URLはリソース指向で、動詞をメソッドに逃がす
エンドポイント命名でいちばん大事な原則はひとつだけ。名詞をURLに、動詞をメソッドに。GET /getOrders や POST /createOrder は、URLの中に「get」「create」という動詞が入っていて、HTTPメソッドと意味がダブっています。
| やりがちなNG | リソース指向の形 | 操作 |
|---|---|---|
POST /createOrder | POST /v1/orders | 注文を作る |
GET /getOrder?id=1 | GET /v1/orders/{orderId} | 注文1件を取る |
POST /updateOrderStatus | PATCH /v1/orders/{orderId} | 注文の一部を直す |
GET /deleteOrder?id=1 | DELETE /v1/orders/{orderId} | 注文を消す |
リソース名は複数形の名詞で統一します(orders、customers)。order と orders が混ざるだけで、利用側は毎回ドキュメントを確認するハメになります。
ひとつ例外があります。注文の「取消」みたいに、業務上の状態遷移が強い操作です。これを無理に DELETE へ寄せると「消したのか、取り消したのか」が読めません。僕は POST /v1/orders/{orderId}/cancel のようにサブリソース風に明示しています。大事なのは、例外を「なんとなく」で増やさず、表に書いてルール化すること。例外がルール化されていれば、Claude Codeも同じ形で書いてくれます。
メソッドとステータスコードを表で固定する
これが今日いちばん持って帰ってほしい表です。実装に入る前に、成功時のコードまで含めて固定します。失敗だけ決めて成功を放置すると、200 と 201 が混ざって利用側が分岐できません。
| 目的 | メソッドとパス | 成功時 | 主なルール |
|---|---|---|---|
| 注文一覧 | GET /v1/orders?status=paid&limit=20&after=ord_123 | 200 OK | カーソルページング。並び順を固定 |
| 注文作成 | POST /v1/orders | 201 Created | Idempotency-Key を受け、Location を返す |
| 注文詳細 | GET /v1/orders/{orderId} | 200 OK | 無ければ 404 |
| 注文更新 | PATCH /v1/orders/{orderId} | 200 OK | 部分更新。空の更新は 400 |
| 注文取消 | POST /v1/orders/{orderId}/cancel | 200 OK | 状態遷移はサブリソース風 |
ステータスコードの使い分けも、初出の意味を添えて固定します。
| コード | 意味 | いつ返すか |
|---|---|---|
200 OK | 成功(中身あり) | 取得・更新が成功 |
201 Created | 作成成功 | POST で新しいリソースができた。Location も返す |
204 No Content | 成功(中身なし) | 削除成功など、返す本文がない |
400 Bad Request | 入力が不正 | 必須漏れ・型違い・バリデーション失敗 |
401 Unauthorized | 未認証 | トークンが無い・期限切れ |
403 Forbidden | 権限不足 | 認証は通ったが、その操作は許されない |
404 Not Found | 対象なし | 指定IDのリソースが存在しない |
409 Conflict | 衝突 | 冪等性キーの食い違い、状態の二重遷移 |
500 Internal Server Error | サーバー側の障害 | 想定外の例外。中身は利用者に漏らさない |
401 と 403 の取り違えはあるあるです。「誰だか分からない」が 401、「あなたが誰かは分かったが、それはダメ」が 403。ここを混ぜると、フロントが「ログイン画面に飛ばす」か「権限エラーを出す」かの判断を誤ります。
エラー応答の形を1種類に決める
ステータスコードは「HTTPの外側の合図」、JSONのエラー本文は「人間とプログラムが読む詳細」。両方を別々に決めるのがコツです。200 OK で { "success": false } を返すと、監視ツールもSDKもリトライ処理も「成功した」と誤解します。これは本当に事故るのでやめましょう。
エラー本文は、全エンドポイントで同じ形に統一します。
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request body has invalid fields.",
"requestId": "req_01J0RESTAPI001",
"details": [
{ "field": "items[0].quantity", "reason": "must be greater than or equal to 1" }
]
}
}
冪等性キーが食い違ったときの 409 も、形は同じです。
{
"error": {
"code": "IDEMPOTENCY_CONFLICT",
"message": "The same Idempotency-Key was used with a different request body.",
"requestId": "req_01J0RESTAPI002"
}
}
役割はこうです。code はプログラムが分岐するための短い名前、message はログや画面で人間が読む説明、requestId は問い合わせ時にサーバーログを探す番号。details はバリデーションエラーのときだけ足せば十分です。この4点が全エラーで揃っていれば、利用側のエラー処理は1回書けば使い回せます。
ページング・バージョニングを曖昧にしない
一覧APIは、件数が少ないうちから必ずページングを決めます。GET /orders で全件返しても最初は動きますが、データが増えた瞬間にレスポンスが重くなり、途中で仕様変更を強いられます。
ここで page=2 方式は避けます。並んでいる途中で誰かが注文を1件足すと、ページの境目がズレて、同じ注文が2回出たり、逆に取りこぼしたりするからです。代わりにカーソル方式を使います。前回レスポンスの nextCursor を、次のリクエストの after に渡すだけ。並び順は createdAt desc, id desc のように固定します。
{
"data": [
{ "id": "ord_100", "status": "paid", "totalCents": 4800, "createdAt": "2026-06-03T09:00:00Z" }
],
"pageInfo": { "nextCursor": "ord_099", "hasMore": true }
}
バージョニングは、利用者に移行時間を渡す仕組みです。判断基準はシンプルで、既存の利用者を壊すかどうか。レスポンスから必須フィールドを消す、型を変える、既定の並び順を変える、新しい必須入力を増やす——これらは破壊的変更なので /v2 を作ります。一方、任意フィールドを1つ足すだけなら /v1 のままでいい。なんでもバージョンを上げると、保守するバージョンが増えて自分の首を絞めます。
コピペで動く:冪等性キーの最小ミドルウェア
冪等性は「あとで足せばいい」と思いがちですが、これが罠でした。僕は本番でネットワークが詰まったとき、クライアントの再送で同じ注文が2件入る事故を出しています。設計段階で入れておけば防げたやつです。
下はExpress 5用の最小ミドルウェア。Idempotency-Key ヘッダと本文のハッシュを保存し、同じキー+同じ本文なら前回の結果を返し、同じキーで本文が違えば 409 を返します。npm i express だけで動きます(保存はメモリなので、本番ではRedis等に置き換えてください)。
import express, { type Request, type Response, type NextFunction } from "express";
import { createHash } from "node:crypto";
const app = express();
app.use(express.json());
// 保存先(本番はRedis等に。ここでは説明用にメモリ)
type Saved = { bodyHash: string; status: number; body: unknown };
const store = new Map<string, Saved>();
// 本文をハッシュ化して「同じ依頼か」を判定する
function hashBody(body: unknown): string {
return createHash("sha256").update(JSON.stringify(body ?? {})).digest("hex");
}
// 冪等性ミドルウェア:POST作成系に挟む
function idempotency(req: Request, res: Response, next: NextFunction) {
const key = req.header("Idempotency-Key");
if (!key) return next(); // キー無しはそのまま通す(任意ヘッダ)
const bodyHash = hashBody(req.body);
const prev = store.get(key);
if (prev) {
// 同じキーで本文が違う → 別の依頼を同じキーで送っている。409で弾く
if (prev.bodyHash !== bodyHash) {
return res.status(409).json({
error: {
code: "IDEMPOTENCY_CONFLICT",
message: "The same Idempotency-Key was used with a different request body.",
requestId: `req_${key.slice(0, 12)}`,
},
});
}
// 同じキー+同じ本文 → 前回の結果をそのまま返す(二重作成しない)
return res.status(prev.status).json(prev.body);
}
// 初回:レスポンスを横取りして保存してから返す
const originalJson = res.json.bind(res);
res.json = (body: unknown) => {
if (res.statusCode < 400) store.set(key, { bodyHash, status: res.statusCode, body });
return originalJson(body);
};
next();
}
app.post("/v1/orders", idempotency, (req: Request, res: Response) => {
const id = `ord_${Math.random().toString(36).slice(2, 8)}`;
// 本来はDB保存。ここでは作成成功を201で返すだけ
res.status(201).location(`/v1/orders/${id}`).json({
data: { id, status: "paid", totalCents: 4800, createdAt: new Date().toISOString() },
});
});
app.listen(3000, () => console.log("http://localhost:3000"));
動作確認はこうです。同じキーで2回叩いても、注文は1件しか増えません。
# 1回目:201 Created(新規作成)
curl -s -X POST http://localhost:3000/v1/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: demo-key-0001234567" \
-d '{"customerId":"cus_1","items":[{"sku":"A","quantity":1}]}'
# 2回目:同じキー+同じ本文 → 前回と同じ結果(二重作成なし)
curl -s -X POST http://localhost:3000/v1/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: demo-key-0001234567" \
-d '{"customerId":"cus_1","items":[{"sku":"A","quantity":1}]}'
このミドルウェアの良いところは、業務ロジックに一切触れないこと。設計段階で「作成系には冪等性キーを必須にする」と決めておけば、実装はこの数十行を挟むだけで済みます。OpenAPIへの落とし込みはOpenAPIとSwagger入門、実装全体の組み立てはClaude CodeでREST API実装が詳しいです。
Claude Codeに「実装前レビュー」をさせる依頼文
ここまで決めたら、いきなり実装させる前に、設計レビューを1回挟みます。僕はこの依頼文をテンプレにしています。
このAPI設計をレビューしてください。実装はまだ書かないでください。
観点:
- リソース名が複数形の名詞か(getXxx/createXxx になっていないか)
- メソッドとステータスコードがMDNの意味に合っているか
- 成功時の 200 / 201 / 204 を使い分けているか
- 400/401/403/404/409/500 のエラーJSONが同じ形か
- 一覧APIにページング(limit, after, nextCursor, hasMore)があり、並び順が固定か
- POST作成系に冪等性キーが要る箇所はないか
- v1 で壊してはいけないレスポンス変更が混ざっていないか
直すべき点を「箇所・問題・修正案」の表で出してください。
ポイントは「実装はまだ書かないで」と先に釘を刺すこと。これが無いと、Claude Codeはレビューを飛ばしていきなりファイルを編集し始めます。設計を直してから実装、テスト観点はAPIテストの自動化、公開前はコードレビューの型、という順番が手戻りを減らします。
よくある質問
Q. URLは複数形 orders と単数形 order のどちらがいい?
A. 複数形の orders に統一します。GET /orders(一覧)と GET /orders/{id}(1件)が自然に並び、混在による確認の手間が消えます。
Q. 更新は PUT と PATCH のどちらを使う?
A. リソース全体を丸ごと置き換えるなら PUT、一部だけ直すなら PATCH。実務では部分更新が多いので、僕は PATCH を基本にして、空の本文は 400 で弾いています。
Q. 削除成功は 200 と 204 のどちら?
A. 返す本文が無いなら 204 No Content、削除後の状態をJSONで返したいなら 200 OK。チームでどちらかに統一すれば、利用側が分岐に悩みません。
Q. 冪等性キーは全エンドポイントに付ける?
A. いいえ。GET や DELETE はそもそも冪等なので不要です。再送で重複が生まれる POST の作成系にだけ付けます。
Q. バージョンはURL(/v1)とヘッダのどちらに入れる?
A. どちらでも成立しますが、ブラウザやcurlで見て分かりやすいURL方式(/v1/orders)が初心者には扱いやすいです。大事なのは方式の統一で、途中で混ぜないことです。
実際に試した結果
正直に言うと、僕は冒頭の POST /createOrder 事故をやるまで、設計は「あとで直せばいい」と思っていました。でも、URLとステータスコードとエラー形式を実装前に表で固定するようにしてから、Claude Codeへの依頼が一発で通るようになりました。出てくるコードがブレないので、レビューで指摘する量がはっきり減ったんです。
いちばん効いたのは冪等性キーをカーソルページングと一緒に設計段階で入れたこと。あとから二重注文対策や一覧の取りこぼし対策を慌てて差し込む、という地獄が消えました。REST API設計は、きれいなURLを考える作業じゃなくて、利用者との約束を表と仕様に落とす作業です。コードを書くのはそのあと。順番を変えるだけで、開発はずっと静かになります。
設計の型やレビュー観点をチームに定着させたい人は研修・相談、テンプレやチェックリストをそのまま使いたい人は教材一覧に整理しています。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
Claude Code の権限拒否から復旧する: 止まった理由を次の安全手順に変える
Claude Code のコマンドが拒否されたとき、焦って許可を広げずに、拒否理由、代替手順、証拠コマンド、再試行条件へ分解する方法。
Claude Codeにビルド→スモークテスト→自動修正を回させる足場の作り方
最小スモークテストの選び方、失敗ログを食わせて直させるループ、回数上限と確認ゲートで暴走を止める方法を、コピペで動くコード付きで紹介します。