用 Claude Code 设计 REST API:端点、错误、分页与幂等性
用 Claude Code 在编码前设计 REST API:OpenAPI、错误格式、分页、幂等性和检查清单。
REST API 并不是写出几条路由就完成了。实现之前,你需要先决定什么算资源、每个操作使用哪种 HTTP 方法、失败时返回什么、列表如何分页,以及同一个请求重复到达时如何处理。如果这些设计保持模糊,Claude Code 仍然能很快写出代码,但前端、移动应用和外部集成会各自理解出不同的契约。
本文的目标,是在让 Claude Code 写实现之前,先把 REST API 设计固定下来。REST 是 Representational State Transfer 的缩写,但在日常开发中,可以把它理解成用 HTTP 规则操作订单、用户、发票等资源的方式。资源是被处理的对象,方法是动作,状态码是结果信号。
官方参考建议使用 MDN 的 HTTP request methods、HTTP response status codes,以及 OpenAPI Specification。截至 2026 年 6 月 3 日,OpenAPI latest 页面指向 3.2.0;本文示例使用 openapi: 3.1.0,因为许多 linter 和代码生成工具对它仍然更稳定。
实现层面的延伸阅读可以看 Claude Code 生产 API 开发、Claude Code API 测试 和 Claude Code 代码审查。可复用模板和检查清单放在 /products/,团队 API 设计审查训练放在 /training/。
先统一词汇
初学者常常卡住,是因为 REST API 的设计词汇很相似。让 Claude Code 实现之前,先让团队对这些词达成一致。
| 术语 | 简单解释 | 本文示例 |
|---|---|---|
| 资源 | API 处理的名词,也是创建、读取、更新、删除的对象 | orders, customers |
| 端点 | URL 和 HTTP 方法的组合 | GET /v1/orders |
| 方法 | 表示要做什么的 HTTP 动词 | GET, POST, PATCH |
| 状态码 | 用数字表达成功、输入错误、未认证等结果 | 200, 201, 400, 404 |
| 校验 | 检查请求是否符合规定形状 | 邮箱格式、最小数量 |
| 幂等性 | 同一操作重复执行,最终状态仍然相同 | 同一 key 只创建一个订单 |
Claude Code 很擅长阅读上下文,但它不能保证猜中你没有写出来的设计意图。只说“做一个创建订单 API”,可能得到 /createOrder、/orders/create 或 POST /orders。先给词汇和设计规则,是提升实现质量最直接的方法。
用三个场景决定设计
同一个订单 API,在不同使用方式下优先级不同。
第一个场景是 B2B SaaS 订单导入。如果合作方重跑同一个夜间 CSV 任务,POST /v1/orders 需要 Idempotency-Key,避免重复创建订单。重试经常发生在故障恢复过程中,幂等性很难事后补得干净。
第二个场景是后台管理列表。运营人员用 status=paid 筛选,翻到下一页,同时另一个人新增订单。单纯的 page=2 可能发生位移。应使用带 limit 和 after 的游标分页,并固定排序,例如 createdAt desc, id desc。
第三个场景是移动应用 API。如果旧版本应用会在用户手机上保留几个月,响应字段名就不能突然改变。新的必填字段或新的错误格式应该进入 /v2,而 /v1 契约继续留在 OpenAPI 中。只新增可选响应字段,通常不需要新版本。
第四个常见场景是内部服务 API。内部调用方更容易一起升级,但监控和告警往往更薄弱。即使是内部 API,状态码和错误格式不一致,也会迫使每个调用方写自己的异常处理。
用图看设计流程
把任务交给 Claude Code 前,先按这个顺序固定设计。这样在编辑文件时,实现不容易偏离契约。
flowchart TD
A["Use cases"] --> B["Resources and endpoints"]
B --> C["OpenAPI contract"]
C --> D["Errors, pagination, idempotency"]
D --> E["Claude Code implementation"]
E --> F["Tests and review"]
编码前固定端点表
第一个交付物不是代码,而是端点表。把这张表交给 Claude Code,路由、OpenAPI、测试和文档就会从同一个契约出发。
| 目的 | 方法和路径 | 成功 | 关键规则 |
|---|---|---|---|
| 订单列表 | 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 | 业务状态迁移保持一致 |
基本原则是“URL 放名词,动作放方法”。不过取消订单这类业务状态迁移,有时写成明确的子操作比强行映射到 DELETE 更易读。重点是把例外写清楚,并在同类操作中保持同一种表达。
把 OpenAPI 作为设计来源
OpenAPI 用机器可读的形式描述路径、参数、请求和响应。当你告诉 Claude Code “以这份 OpenAPI 契约为准实现”时,就把实现自由度限制在合理范围内。
openapi: 3.1.0
info:
title: Acme Orders API
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/v1/orders:
get:
operationId: listOrders
summary: List orders
parameters:
- $ref: "#/components/parameters/StatusParam"
- $ref: "#/components/parameters/LimitParam"
- $ref: "#/components/parameters/AfterParam"
responses:
"200":
description: Orders are returned in a stable cursor order.
content:
application/json:
schema:
$ref: "#/components/schemas/OrderListResponse"
"400":
$ref: "#/components/responses/BadRequest"
post:
operationId: createOrder
summary: Create an order
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOrderRequest"
responses:
"201":
description: Order created.
headers:
Location:
schema:
type: string
description: URL of the created order.
content:
application/json:
schema:
$ref: "#/components/schemas/OrderResponse"
"400":
$ref: "#/components/responses/BadRequest"
"409":
$ref: "#/components/responses/Conflict"
/v1/orders/{orderId}:
get:
operationId: getOrder
summary: Get one order
parameters:
- $ref: "#/components/parameters/OrderId"
responses:
"200":
description: Order found.
content:
application/json:
schema:
$ref: "#/components/schemas/OrderResponse"
"404":
$ref: "#/components/responses/NotFound"
patch:
operationId: updateOrder
summary: Update mutable order fields
parameters:
- $ref: "#/components/parameters/OrderId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateOrderRequest"
responses:
"200":
description: Order updated.
content:
application/json:
schema:
$ref: "#/components/schemas/OrderResponse"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
components:
parameters:
OrderId:
name: orderId
in: path
required: true
schema:
type: string
StatusParam:
name: status
in: query
schema:
type: string
enum: [draft, paid, canceled]
LimitParam:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
AfterParam:
name: after
in: query
schema:
type: string
description: Cursor returned by the previous response.
IdempotencyKey:
name: Idempotency-Key
in: header
required: false
schema:
type: string
minLength: 16
maxLength: 128
schemas:
CreateOrderRequest:
type: object
required: [customerId, items]
properties:
customerId:
type: string
items:
type: array
minItems: 1
items:
type: object
required: [sku, quantity]
properties:
sku:
type: string
quantity:
type: integer
minimum: 1
UpdateOrderRequest:
type: object
minProperties: 1
properties:
status:
type: string
enum: [draft, paid, canceled]
OrderResponse:
type: object
required: [data]
properties:
data:
$ref: "#/components/schemas/Order"
OrderListResponse:
type: object
required: [data, pageInfo]
properties:
data:
type: array
items:
$ref: "#/components/schemas/Order"
pageInfo:
type: object
required: [nextCursor, hasMore]
properties:
nextCursor:
type: [string, "null"]
hasMore:
type: boolean
Order:
type: object
required: [id, status, totalCents, createdAt]
properties:
id:
type: string
status:
type: string
totalCents:
type: integer
createdAt:
type: string
format: date-time
ErrorResponse:
type: object
required: [error]
properties:
error:
type: object
required: [code, message, requestId]
properties:
code:
type: string
message:
type: string
requestId:
type: string
details:
type: array
items:
type: object
responses:
BadRequest:
description: Request validation failed.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
Conflict:
description: Idempotency key conflicts with a different request.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
NotFound:
description: Resource was not found.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
将它保存为 openapi.yaml,并明确要求 Claude Code 不要提出偏离这份契约的实现。之后可以用 npx @redocly/cli lint openapi.yaml 之类的 linter 检查语法和引用。如果要从 OpenAPI 生成 TypeScript 类型或客户端,也应先由人审查端点表的业务含义。
统一错误格式和状态码
状态码是 HTTP 层的外部信号,JSON 错误正文是人和程序读取的细节。返回 200 OK 加 { "success": false } 会让监控、SDK 和重试逻辑把失败当成成功。先固定表格:输入错误是 400,未认证是 401,权限不足是 403,资源不存在是 404,重复或幂等性冲突是 409。
{
"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"
}
]
}
}
{
"error": {
"code": "IDEMPOTENCY_CONFLICT",
"message": "The same Idempotency-Key was used with a different request body.",
"requestId": "req_01J0RESTAPI002"
}
}
{
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order was not found.",
"requestId": "req_01J0RESTAPI003"
}
}
给初学者的解释是:code 是程序分支用的短名称,message 是日志或界面给人看的说明,requestId 是排查时定位服务端日志的编号。details 主要在字段级校验错误时使用。
不要让分页、幂等性和版本含糊
列表 API 从一开始就应该有分页。数据很少时返回所有订单也能工作,但表变大后再改响应形状就会成为破坏性变更。使用游标分页时,客户端把上一次响应的 nextCursor 作为下一次请求的 after。
{
"data": [
{
"id": "ord_100",
"status": "paid",
"totalCents": 4800,
"createdAt": "2026-06-03T09:00:00Z"
}
],
"pageInfo": {
"nextCursor": "ord_099",
"hasMore": true
}
}
幂等性用于避免重复请求产生重复效果。POST /orders 尤其敏感,因为客户端在网络失败后会重试。将 Idempotency-Key 和请求正文 hash 保存一段时间。相同 key 和相同正文到达时返回上一次结果;相同 key 但正文不同,则返回 409。
版本管理给调用方迁移时间。删除必填响应字段、改变类型、改变默认排序、增加新的必填请求字段,都是破坏性变更,应进入 /v2。只新增可选响应字段,通常可以留在 /v1。
让 Claude Code 先做设计审查
实现前可以使用下面的具体 prompt。
Review this OpenAPI design.
Check:
- Resource names are nouns
- HTTP methods and status codes match MDN semantics
- 400/401/403/404/409/500 errors share the same JSON shape
- List pagination is unambiguous
- POST creation endpoints that need idempotency keys are identified
- No response change breaks the v1 contract
- Return a table of issues to fix before TypeScript implementation and tests
这样 Claude Code 在编辑文件前会先像设计审查者一样工作。只有在保存经过审查的 OpenAPI 后,再请求它实现。
常见失败案例
失败案例一是动作式端点。POST /createOrder、POST /updateOrderStatus、GET /deleteOrder 混在一起时,URL 很难表达资源和操作。优先使用 POST /v1/orders、PATCH /v1/orders/{orderId}、DELETE /v1/orders/{orderId},并把例外写进文档。
失败案例二是状态码不一致。输入错误返回 500,资源不存在返回 200,创建成功有时 200 有时 201,客户端就无法可靠分支。成功码也需要像错误码一样固定。
失败案例三是分页语义不清。page=2 如果没有说明按创建时间还是更新时间排序,新增数据会造成重复或遗漏。排序方式、最大 limit、是否有下一页,都应写进 OpenAPI。
失败案例四是校验只藏在实现里。代码里有 quantity >= 1,但 OpenAPI 没有写,前端和生成的 SDK 就无法复用同一规则。约束条件要同时放在规范和实现里。
审查清单
- 资源名统一使用复数名词
- 方法符合
GET、POST、PATCH、DELETE的语义 - 成功码区分
200、201、204 400、401、403、404、409、500使用相同 JSON 形状- 列表 API 包含
limit、after、nextCursor、hasMore limit的默认值和最大值已经写明- 创建接口在重试时不会产生重复数据
- 破坏性变更进入
/v2的标准已经写下 - 校验规则出现在 OpenAPI 中
- 交给 Claude Code 前,端点表和 OpenAPI 内容一致
REST API 设计不是把 URL 设计得好看,而是把对调用方的承诺写成可读文本和机器可读规范。Claude Code 可以加快实现速度,但系统必须遵守哪些承诺,仍然需要人先决定。
在 Masa 的订单 API 上实际尝试后,先准备 OpenAPI、端点表和错误 JSON,再让 Claude Code 实现,明显减少了审查返工。因为在设计阶段就加入了 Idempotency-Key 和游标分页,后续也不需要再临时修补重复订单和列表漏项问题。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code权限安全阶梯:逐步放开访问而不失控
从只读到有限编辑、验证命令和部署检查的 Claude Code 权限升级流程。
Claude Code 小PR证据包:让小改动真正可审查
用差异、验证命令、公开URL、CTA路径和回滚说明,把Claude Code的小PR变得可审查。
Claude Code 提交前 Review Gate:同时检查差异、测试、公开 URL 和 CTA
提交前用 Claude Code 审查差异范围、build、公开 URL、Gumroad 链接、咨询 CTA、缺少测试和无关文件。