Claude CodeでAPI設計を実務化する方法:OpenAPI・テスト・破壊的変更チェック
Claude CodeでAPI設計を実務レベルにする手順。OpenAPI、エラー、テスト、破壊的変更まで解説。
API設計は、かっこいいURLを考える作業ではありません。チームと利用者の間で「どんな入力を受け取り、どんな出力を返し、失敗したときにどう知らせるか」を先に決める作業です。
この約束が曖昧なまま実装すると、フロントエンド、モバイルアプリ、外部連携、テスト、運用監視がそれぞれ別の解釈で進みます。後から直すほど高くつくため、最初に小さくても明確な契約を作るほうが安全です。
Claude Codeは、その契約作りの相棒として使えます。ただし「APIを作って」と丸投げするのではなく、OpenAPI、HTTPの意味、JSON Schema、セキュリティ、破壊的変更の観点を渡してレビューさせるのが実務向きです。
公式情報は、OpenAPI Specification、RFC 9110 HTTP Semantics、JSON Schema docs、OWASP API Security Top 10を基準にします。実装まで進める記事としては、本番API開発ガイド、APIテスト自動化、APIバージョニング戦略も合わせて読むと流れがつながります。
API設計とは何か
APIは「別のプログラムから使われる画面」です。人間向けの画面ならラベルや配置で意図を伝えられますが、APIではURL、HTTPメソッド、ステータスコード、JSONの項目名、エラー形式がそのまま説明になります。
初心者向けに言い換えると、API設計は次の5つを決めることです。
| 決めるもの | 平易な説明 | 例 |
|---|---|---|
| リソース | APIで扱う名詞 | orders, customers, invoices |
| 操作 | その名詞に何をするか | GET, POST, PATCH, DELETE |
| スキーマ | JSONの形と入力ルール | customerIdは文字列、itemsは1件以上 |
| エラー | 失敗時の返し方 | 400、401、404、422を使い分ける |
| 互換性 | 既存利用者を壊さない約束 | 必須項目の追加は破壊的変更として扱う |
RESTという言葉も難しく見えますが、実務では「名詞のURLにHTTPメソッドで操作を表す」と理解すると入りやすいです。POST /orders/createよりPOST /orders、GET /getOrder?id=1よりGET /orders/1のほうが、利用者もテストも読みやすくなります。
Claude Codeで進める全体像
Claude Codeには、いきなり実装を頼むより、次の順番で頼むと品質が安定します。
flowchart TD
A["業務ルールを短く整理"] --> B["OpenAPIの下書きを作る"]
B --> C["HTTP・スキーマ・セキュリティ観点でレビュー"]
C --> D["モックサーバーとAPIテストを生成"]
D --> E["破壊的変更チェックをCIに入れる"]
E --> F["実装・ドキュメント・営業資料へ展開"]
ポイントは、Claude Codeを「コード生成係」ではなく「設計レビュー係」として使うことです。OpenAPIはAPI契約を機械で読める形にしたもの、JSON SchemaはJSONの形を検証するための語彙、HTTPステータスコードは失敗や成功の意味を伝える共通語です。
Masaが小さな検証プロジェクトで試したときも、最初にURLだけを生成させた案は悪くありませんでした。しかし、認証、ページネーション、idempotency key、エラーの粒度を後から足すと差分が膨らみました。最初のプロンプトで「利用者が失敗したときの復旧方法」まで聞かせるほうが、結果として短い実装になります。
コピペで試せるスターターキット
ここでは外部依存なしで動く最小例を使います。OpenAPIの考え方、モック、破壊的変更チェックを同じ題材で試せるようにします。
mkdir api-design-lab
cd api-design-lab
mkdir docs examples
node --version
次に docs/openapi.yaml を作ります。OpenAPIの公式ページでは最新公開版も確認できますが、ここではツール対応が広い3.1形式で小さく始めます。
openapi: 3.1.0
info:
title: Orders API
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/v1/orders:
post:
summary: Create an order
operationId: createOrder
security:
- bearerAuth: []
parameters:
- name: Idempotency-Key
in: header
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOrderRequest"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"422":
description: Validation error
content:
application/json:
schema:
$ref: "#/components/schemas/Problem"
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
schemas:
CreateOrderRequest:
type: object
required: [customerId, items]
properties:
customerId:
type: string
minLength: 3
items:
type: array
minItems: 1
items:
type: object
required: [sku, quantity]
properties:
sku:
type: string
minLength: 3
quantity:
type: integer
minimum: 1
Order:
type: object
required: [id, status, customerId, total]
properties:
id:
type: string
status:
type: string
enum: [accepted, cancelled]
customerId:
type: string
total:
type: integer
Problem:
type: object
required: [type, title, status, detail]
properties:
type:
type: string
title:
type: string
status:
type: integer
detail:
type: string
errors:
type: array
items:
type: object
次に examples/mock-server.mjs を作ります。Node標準ライブラリだけで動くので、API設計の検証に集中できます。
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
function readJson(req) {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk) => {
body += chunk;
if (body.length > 1_000_000) req.destroy(new Error("Body too large"));
});
req.on("end", () => {
if (!body) return resolve({});
try {
resolve(JSON.parse(body));
} catch (error) {
reject(error);
}
});
req.on("error", reject);
});
}
function send(res, status, body, headers = {}) {
res.writeHead(status, {
"content-type": "application/json; charset=utf-8",
"x-content-type-options": "nosniff",
...headers,
});
res.end(JSON.stringify(body, null, 2));
}
function problem(status, title, detail, errors = []) {
return {
type: "https://example.com/problems/request",
title,
status,
detail,
errors,
};
}
function validateOrder(input) {
const errors = [];
if (typeof input.customerId !== "string" || input.customerId.length < 3) {
errors.push({
path: "customerId",
message: "customerId must be a string with 3+ characters",
});
}
if (!Array.isArray(input.items) || input.items.length === 0) {
errors.push({ path: "items", message: "items must contain at least one item" });
}
for (const [index, item] of (input.items ?? []).entries()) {
if (typeof item.sku !== "string" || item.sku.length < 3) {
errors.push({
path: `items.${index}.sku`,
message: "sku must be a string with 3+ characters",
});
}
if (!Number.isInteger(item.quantity) || item.quantity < 1) {
errors.push({
path: `items.${index}.quantity`,
message: "quantity must be a positive integer",
});
}
}
return errors;
}
const server = createServer(async (req, res) => {
const url = new URL(req.url ?? "/", "http://localhost");
if (req.method === "GET" && url.pathname === "/health") {
return send(res, 200, { ok: true });
}
const customerMatch = url.pathname.match(/^\/v1\/customers\/([a-z0-9-]+)$/);
if (req.method === "GET" && customerMatch) {
return send(res, 200, {
id: customerMatch[1],
name: "Aki Tanaka",
plan: "pro",
});
}
if (req.method === "POST" && url.pathname === "/v1/orders") {
const idempotencyKey = req.headers["idempotency-key"];
if (!idempotencyKey) {
return send(
res,
400,
problem(400, "Missing Idempotency-Key", "POST /v1/orders requires the header.")
);
}
try {
const body = await readJson(req);
const errors = validateOrder(body);
if (errors.length > 0) {
return send(res, 422, problem(422, "Invalid request body", "Fix errors.", errors));
}
return send(
res,
201,
{
id: `ord_${randomUUID()}`,
status: "accepted",
customerId: body.customerId,
total: 4200,
},
{ location: "/v1/orders/example" }
);
} catch {
return send(res, 400, problem(400, "Malformed JSON", "Request body must be JSON."));
}
}
return send(res, 404, problem(404, "Not found", `${req.method} ${url.pathname} is undefined.`));
});
server.listen(3000, () => {
console.log("Mock API running at http://localhost:3000");
});
別のターミナルで試します。
node examples/mock-server.mjs
curl -i http://localhost:3000/health
curl -i -X POST http://localhost:3000/v1/orders \
-H "content-type: application/json" \
-H "idempotency-key: demo-001" \
-d '{"customerId":"cus_123","items":[{"sku":"book-1","quantity":2}]}'
curl -i -X POST http://localhost:3000/v1/orders \
-H "content-type: application/json" \
-H "idempotency-key: demo-002" \
-d '{"customerId":"x","items":[]}'
最後に examples/contract-check.mjs を作ります。これは意図的に破壊的変更を検出するサンプルです。
import assert from "node:assert/strict";
const previous = {
paths: {
"/v1/orders": {
post: {
request: {
required: ["customerId", "items"],
properties: ["customerId", "items", "couponCode"],
},
response: {
required: ["id", "status", "customerId", "total"],
properties: ["id", "status", "customerId", "total"],
},
},
},
},
};
const next = structuredClone(previous);
next.paths["/v1/orders"].post.request.required.push("shippingAddress");
next.paths["/v1/orders"].post.response.properties =
next.paths["/v1/orders"].post.response.properties.filter((name) => name !== "total");
function diffContract(oldSpec, newSpec) {
const breaking = [];
for (const [path, methods] of Object.entries(oldSpec.paths)) {
for (const [method, oldOperation] of Object.entries(methods)) {
const newOperation = newSpec.paths[path]?.[method];
if (!newOperation) {
breaking.push(`${method.toUpperCase()} ${path} was removed`);
continue;
}
const oldRequired = new Set(oldOperation.request.required);
for (const field of newOperation.request.required) {
if (!oldRequired.has(field)) {
breaking.push(`${method.toUpperCase()} ${path} now requires "${field}"`);
}
}
const newResponseFields = new Set(newOperation.response.properties);
for (const field of oldOperation.response.properties) {
if (!newResponseFields.has(field)) {
breaking.push(`${method.toUpperCase()} ${path} removed response "${field}"`);
}
}
}
}
return breaking;
}
const breaking = diffContract(previous, next);
console.log(breaking.join("\n") || "No breaking changes found");
assert.equal(breaking.length, 0, "Breaking API changes detected");
node examples/contract-check.mjs
このスクリプトは失敗して正解です。新しい必須項目の追加とレスポンス項目の削除を検出できれば、CIで同じ種類の事故を止められます。
Claude Codeへの実務プロンプト
実務では、Claude Codeに「作る」「見る」「試す」「壊していないか確認する」を分けて頼みます。1つの巨大プロンプトで実装まで進めるより、レビュー可能な差分を小さく保てます。
claude -p "
EC注文APIのOpenAPI下書きを docs/openapi.yaml に作成してください。
対象は customers, orders, invoices です。
各エンドポイントに summary, operationId, requestBody, responses, examples を入れてください。
認証が必要な操作には bearerAuth を付けてください。
OpenAPI 3.1 と JSON Schema の書き方に合わせてください。
"
claude -p "
docs/openapi.yaml をAPI設計レビューとして読んでください。
まずFindingsを重大度順に出し、まだファイルは編集しないでください。
観点: RFC 9110に沿ったHTTPメソッドとステータス、曖昧なスキーマ、
ページネーション、idempotency、認証、OWASP API Securityの典型リスク。
"
claude -p "
docs/openapi.yaml から、Node.jsのモックサーバーとAPIテスト例を生成してください。
正常系、認証エラー、バリデーションエラー、存在しないリソースを含めてください。
長い行は150文字以内にし、READMEに実行コマンドも書いてください。
"
claude -p "
現在の docs/openapi.yaml と HEAD の版を比較してください。
既存クライアントに対する破壊的変更を先に列挙してください。
削除されたpath、追加された必須項目、レスポンス項目の削除、
ステータスコードや認証スコープの変更を確認してください。
"
この分け方にすると、Claude Codeの出力をそのまま信じるのではなく、レビュー対象として扱えます。API設計は生成速度よりも、利用者が壊れずに使い続けられることのほうが重要です。
3つ以上の現実的なユースケース
1つ目はSaaSの注文APIです。管理画面、請求、メール通知、外部会計ソフトが同じ注文データを参照するため、Orderのスキーマが曖昧だと全員が困ります。totalの通貨、税抜き税込み、キャンセル状態を最初に決める必要があります。
2つ目はモバイルアプリのプロフィールAPIです。アプリは古いバージョンが数か月残るため、レスポンス項目を急に消すとクラッシュにつながります。新しい項目を追加するときも、古いアプリが無視できる形にします。
3つ目はB2Bのパートナー連携APIです。相手企業の開発者は社内事情を知りません。エラーコード、レート制限、再送してよい条件、サンドボックス環境がないと、問い合わせ対応の時間が増えます。
4つ目は社内管理APIです。社内向けでも権限境界は必要です。特に「自分の会社の注文だけ見える」ようなオブジェクト単位の認可は、OWASP API Securityでも重要なリスクとして扱われます。社内だから安全、ではなく、誤操作と過剰権限を前提に設計します。
失敗例と落とし穴
ありがちな失敗は、URLに動詞を詰め込むことです。/cancelOrder、/getUserOrders、/updateOrderStatusが増えると、似た機能が別名で乱立します。リソース中心にPOST /orders/{id}/cancellationやPATCH /orders/{id}へ寄せると整理しやすくなります。
次の失敗は、全部を200で返すことです。業務エラーをHTTP 200に入れると、監視、SDK、ロードバランサー、クライアントのリトライ判断が難しくなります。構文が壊れているなら400、認証がないなら401、権限がないなら403、存在しないなら404、意味的な入力不備なら422、というように意味を分けます。
POSTの再送対策を忘れるのも危険です。注文作成や決済開始は、通信タイムアウト後にクライアントが再送することがあります。Idempotency-Keyのような一意キーを設計に入れておくと、同じ操作を二重実行しにくくなります。
スキーマのnullと未指定を混ぜるのも落とし穴です。nickname: nullは「消す」なのか「未設定」なのかを決めないと、クライアントごとに挙動が分かれます。JSON Schemaではrequired、型、minLength、additionalPropertiesを明示して、曖昧さを減らします。
最後に、レスポンスの項目削除を軽く見ないことです。サーバー側では小さな整理でも、利用者にとっては本番障害です。項目を消す前に非推奨期間、移行先、通知、契約テストを用意します。
バージョニング・エラー・スキーマ・セキュリティ
バージョニングは、最初から/v1を入れるだけでは不十分です。何を破壊的変更とみなすかをチームで決める必要があります。新しい任意項目の追加は多くの場合安全ですが、必須リクエスト項目の追加、既存レスポンス項目の削除、enum値の意味変更、認証スコープの強化は既存利用者を壊す可能性があります。
エラー設計では、利用者が次に何をすればよいかを返します。Invalid requestだけでは不親切です。errors配列にpathとmessageを入れ、入力画面でどの項目を直せばよいか分かる形にします。ただし、本番エラーでスタックトレースや内部SQLを返してはいけません。
スキーマ設計では、サンプルだけでなく制約を書きます。type: stringだけでは短すぎます。IDの形式、最小文字数、配列の最小件数、ページサイズの上限、日時のタイムゾーンを決めると、クライアントの実装が安定します。
セキュリティでは、認証と認可を分けて考えます。認証は「誰か」を確認すること、認可は「その人がこの注文を見てよいか」を確認することです。Bearer tokenを使っていても、/orders/{id}で他人の注文を見られるなら設計ミスです。APIキーをURLクエリに入れない、機密項目をレスポンスに混ぜない、レート制限と監査ログを用意する、といった基本も最初に入れます。
Claude Codeを収益につなげるCTA
API設計の記事を読む人は、単に知識を集めているだけではありません。多くの場合、外部連携を公開したい、モバイルアプリのバックエンドを固めたい、チームのレビュー基準を作りたい、という具体的な課題を持っています。
ClaudeCodeLabでは、Claude Codeを使ったAPI設計レビュー、OpenAPI整備、テスト自動化、破壊的変更チェックの導入支援を扱えます。チーム導入を急ぐ場合は研修・相談へ、個人で型を学びたい場合は無料リソースから始める導線が自然です。
営業色を強くしすぎる必要はありません。この記事で示したように、実際に動くモック、失敗する契約チェック、公式仕様へのリンクがあれば、読者は「この人に相談すると具体的に進みそうだ」と判断できます。
この記事で実際に試した結果
この記事のコードは、Node v24.14.1で mock-server.mjs を起動し、GET /health が200、正常な POST /v1/orders が201、空の items が422を返すことを確認しました。contract-check.mjs は意図どおり失敗し、追加された必須項目と削除されたレスポンス項目を表示しました。薄い設計記事ではなく、Claude Codeに渡すプロンプト、OpenAPIの骨格、モック、失敗例、CIに入れたい考え方まで一通り検証できる内容にしています。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。