用 Claude Code 做实用 API 设计:OpenAPI、测试与破坏性变更检查
用 Claude Code 设计可靠 REST API:OpenAPI 流程、Mock、测试、版本、安全与常见坑。
API 设计不是给 URL 起好看的名字,而是先约定清楚:客户端可以发送什么、服务端会返回什么、失败时怎样说明原因。这个约定越清楚,前端、移动端、合作伙伴、测试和运维就越少猜测。
如果一开始只让 Claude Code “帮我写一个 API”,它可能会生成能跑的代码,但未必会处理版本、错误、权限、重试和破坏性变更。更好的用法是把 Claude Code 当成 API 设计审阅者:先起草 OpenAPI,再审查端点、生成 Mock 和测试,最后检查是否破坏已有客户端。
工作时以官方资料为准:OpenAPI Specification、RFC 9110 HTTP Semantics、JSON Schema docs、OWASP API Security Top 10。继续深入实现时,可以阅读生产级 API 开发、API 测试自动化和API 版本管理。
API 设计到底设计什么
API 可以理解为“给程序看的界面”。人类界面可以靠按钮、文案和布局表达意图,API 则靠路径、HTTP method、状态码、JSON 字段、schema、示例和错误体表达意图。
初学者可以先记住五件事。
| 要决定的事 | 通俗解释 | 例子 |
|---|---|---|
| Resource | API 暴露的名词 | orders, customers, invoices |
| Operation | 对名词做什么 | GET, POST, PATCH, DELETE |
| Schema | JSON 的形状和限制 | items 至少有一项 |
| Error | 失败时如何返回 | 400、401、403、404、422 加详细信息 |
| Compatibility | 怎样不弄坏已有客户端 | 新增必填请求字段属于破坏性变更 |
REST 不需要一开始讲得很玄。实务上先做到:URL 用名词,动作交给 HTTP method。POST /orders 通常比 POST /orders/create 清楚,GET /orders/ord_123 也比 GET /getOrder?id=ord_123 更容易测试和维护。
Claude Code 的实用流程
不要一次性要求 Claude Code “设计、实现、测试、写文档”。把流程拆小,才能审查每一步。
flowchart TD
A["整理业务规则"] --> B["起草 OpenAPI 合同"]
B --> C["审查 HTTP、schema、安全风险"]
C --> D["生成 Mock 和 API 测试"]
D --> E["在 CI 中检查破坏性变更"]
E --> F["实现、写文档并对外发布"]
OpenAPI 是 HTTP API 的机器可读合同,JSON Schema 是描述 JSON 形状和限制的词汇,HTTP 状态码是表达成功和失败的共同语言。Claude Code 可以把这些串起来,但最终依据仍然是官方规范和测试结果。
Masa 在小型验证项目里试过:只让模型列端点,结果看起来不错,但后面补认证、分页、idempotency key 和错误细节时,差异迅速变大。更稳的做法是在第一轮就要求它说明“客户端失败后如何恢复”。
可以直接复制运行的示例
下面的例子不依赖外部包。你可以先试 OpenAPI、Mock server 和破坏性变更检查,再把模式搬到真实项目。
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 标准库。
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 -p "
请为电商订单 API 在 docs/openapi.yaml 创建 OpenAPI 草稿。
资源包括 customers, orders, invoices。
每个端点都要有 summary, operationId, requestBody, responses, examples 和 bearerAuth。
请使用 OpenAPI 3.1 与 JSON Schema 风格的约束。
"
claude -p "
请以 API 设计审阅者身份阅读 docs/openapi.yaml。
先按严重程度列出 Findings,暂时不要编辑文件。
检查 RFC 9110 的 method/status 语义、模糊 schema、分页、idempotency、
认证以及 OWASP API Security 常见风险。
"
claude -p "
请根据 docs/openapi.yaml 生成 Node.js Mock server 和 API test 示例。
覆盖成功、认证失败、验证失败、资源不存在。
长行控制在150字符以内,并在 README 写运行命令。
"
claude -p "
请比较当前 docs/openapi.yaml 与 HEAD 中的版本。
先列出破坏性变更,再建议修改。
检查删除的 path、新增必填字段、删除响应字段、状态码变化和认证 scope 变化。
"
现实中的用例
第一类是 SaaS 订单 API。管理后台、计费任务、邮件通知、会计系统都会读取同一个 Order。如果 total、币种、税额和取消状态没有定义清楚,后续集成都会各自猜测。
第二类是移动端 profile API。旧版本 App 可能在线上停留数月,响应字段突然删除会导致崩溃。新增字段要做到旧客户端可以忽略,并给迁移留时间。
第三类是 B2B partner API。外部开发者不了解你的内部约定,需要稳定错误码、rate limit、重试规则、sandbox 和示例。没有这些,客服工单就会变成文档。
第四类是内部管理 API。内部系统也需要对象级授权。用户不能因为知道某个订单 ID 就读取别的租户订单。认证是确认“是谁”,授权是确认“能不能访问这个对象”。
常见失败与坑
常见失败之一是把动词塞进 URL,比如 /cancelOrder、/getUserOrders。端点越多,命名越乱。先用资源建模,再用 HTTP method 或子资源表达动作,会更稳定。
第二个坑是所有业务错误都返回 200。这样监控、SDK、重试和客户端分支都会变复杂。400、401、403、404、422 应该分别表达不同失败含义。
第三个坑是 POST 没有重试安全。创建订单或开始支付时,客户端可能因为超时重发。提前设计 Idempotency-Key,可以避免重复执行。
第四个坑是 schema 只写示例,不写约束。null 是清空还是未知?分页上限是多少?数组能不能为空?这些都应该写进 schema。
版本、错误、Schema 与安全
版本控制不只是路径里加 /v1。团队要先定义哪些是破坏性变更:新增必填请求字段、删除响应字段、改变状态码语义、收紧权限、修改 enum 含义,都可能影响已有客户端。
错误设计要告诉调用方下一步怎么做。只返回 Invalid request 不够。建议固定 type、title、status、detail,必要时加字段级 errors。生产环境不要暴露 stack trace、SQL 或内部 ID。
Schema 设计要写约束,而不是只给例子。ID 格式、最小长度、数组上限、分页默认值、时区规则都能减少客户端猜测。
安全设计要分清认证和授权。Bearer token 只能说明“这个请求来自谁”,不能自动保证他能访问这个订单。避免把 API key 放进 query,限制敏感字段返回,对高风险操作做 rate limit,并记录审计日志。
变现与咨询 CTA
搜索 API 设计的读者通常已经有真实项目:公开集成、移动后端、团队审查规则或外部合作接口。文章需要展示的不只是知识点,而是可落地的工程流程。
ClaudeCodeLab 可以协助团队做 Claude Code API 设计审阅、OpenAPI 整理、测试自动化和破坏性变更检查。团队可以从培训与咨询开始,个人开发者可以先看免费资源。
实际验证结果
本文代码使用 Node v24.14.1 验证。GET /health 返回 200,合法的 POST /v1/orders 返回 201,空 items 返回 422。contract-check.mjs 会按预期失败,并显示新增必填字段和删除响应字段。这样读者可以从提示词、OpenAPI、Mock、错误设计到 CI 兼容性检查完整走一遍。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。