Tips & Tricks (更新: 2026/6/3)

用 Claude Code 设计 REST API:端点、错误、分页与幂等性

用 Claude Code 在编码前设计 REST API:OpenAPI、错误格式、分页、幂等性和检查清单。

用 Claude Code 设计 REST API:端点、错误、分页与幂等性

REST API 并不是写出几条路由就完成了。实现之前,你需要先决定什么算资源、每个操作使用哪种 HTTP 方法、失败时返回什么、列表如何分页,以及同一个请求重复到达时如何处理。如果这些设计保持模糊,Claude Code 仍然能很快写出代码,但前端、移动应用和外部集成会各自理解出不同的契约。

本文的目标,是在让 Claude Code 写实现之前,先把 REST API 设计固定下来。REST 是 Representational State Transfer 的缩写,但在日常开发中,可以把它理解成用 HTTP 规则操作订单、用户、发票等资源的方式。资源是被处理的对象,方法是动作,状态码是结果信号。

官方参考建议使用 MDN 的 HTTP request methodsHTTP 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/createPOST /orders。先给词汇和设计规则,是提升实现质量最直接的方法。

用三个场景决定设计

同一个订单 API,在不同使用方式下优先级不同。

第一个场景是 B2B SaaS 订单导入。如果合作方重跑同一个夜间 CSV 任务,POST /v1/orders 需要 Idempotency-Key,避免重复创建订单。重试经常发生在故障恢复过程中,幂等性很难事后补得干净。

第二个场景是后台管理列表。运营人员用 status=paid 筛选,翻到下一页,同时另一个人新增订单。单纯的 page=2 可能发生位移。应使用带 limitafter 的游标分页,并固定排序,例如 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_123200 OK使用稳定排序的游标分页
创建订单POST /v1/orders201 Created接受 Idempotency-Key 并返回 Location
订单详情GET /v1/orders/{orderId}200 OK不存在则返回 404
更新订单PATCH /v1/orders/{orderId}200 OK部分更新;空更新返回 400
取消订单POST /v1/orders/{orderId}/cancel200 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 /createOrderPOST /updateOrderStatusGET /deleteOrder 混在一起时,URL 很难表达资源和操作。优先使用 POST /v1/ordersPATCH /v1/orders/{orderId}DELETE /v1/orders/{orderId},并把例外写进文档。

失败案例二是状态码不一致。输入错误返回 500,资源不存在返回 200,创建成功有时 200 有时 201,客户端就无法可靠分支。成功码也需要像错误码一样固定。

失败案例三是分页语义不清。page=2 如果没有说明按创建时间还是更新时间排序,新增数据会造成重复或遗漏。排序方式、最大 limit、是否有下一页,都应写进 OpenAPI。

失败案例四是校验只藏在实现里。代码里有 quantity >= 1,但 OpenAPI 没有写,前端和生成的 SDK 就无法复用同一规则。约束条件要同时放在规范和实现里。

审查清单

  • 资源名统一使用复数名词
  • 方法符合 GETPOSTPATCHDELETE 的语义
  • 成功码区分 200201204
  • 400401403404409500 使用相同 JSON 形状
  • 列表 API 包含 limitafternextCursorhasMore
  • limit 的默认值和最大值已经写明
  • 创建接口在重试时不会产生重复数据
  • 破坏性变更进入 /v2 的标准已经写下
  • 校验规则出现在 OpenAPI 中
  • 交给 Claude Code 前,端点表和 OpenAPI 内容一致

REST API 设计不是把 URL 设计得好看,而是把对调用方的承诺写成可读文本和机器可读规范。Claude Code 可以加快实现速度,但系统必须遵守哪些承诺,仍然需要人先决定。

在 Masa 的订单 API 上实际尝试后,先准备 OpenAPI、端点表和错误 JSON,再让 Claude Code 实现,明显减少了审查返工。因为在设计阶段就加入了 Idempotency-Key 和游标分页,后续也不需要再临时修补重复订单和列表漏项问题。

#claude-code #rest-api #openapi #typescript #backend
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。