Use Cases (更新: 2026/6/2)

用 Claude Code 做实用 API 设计:OpenAPI、测试与破坏性变更检查

用 Claude Code 设计可靠 REST API:OpenAPI 流程、Mock、测试、版本、安全与常见坑。

用 Claude Code 做实用 API 设计:OpenAPI、测试与破坏性变更检查

API 设计不是给 URL 起好看的名字,而是先约定清楚:客户端可以发送什么、服务端会返回什么、失败时怎样说明原因。这个约定越清楚,前端、移动端、合作伙伴、测试和运维就越少猜测。

如果一开始只让 Claude Code “帮我写一个 API”,它可能会生成能跑的代码,但未必会处理版本、错误、权限、重试和破坏性变更。更好的用法是把 Claude Code 当成 API 设计审阅者:先起草 OpenAPI,再审查端点、生成 Mock 和测试,最后检查是否破坏已有客户端。

工作时以官方资料为准:OpenAPI SpecificationRFC 9110 HTTP SemanticsJSON Schema docsOWASP API Security Top 10。继续深入实现时,可以阅读生产级 API 开发API 测试自动化API 版本管理

API 设计到底设计什么

API 可以理解为“给程序看的界面”。人类界面可以靠按钮、文案和布局表达意图,API 则靠路径、HTTP method、状态码、JSON 字段、schema、示例和错误体表达意图。

初学者可以先记住五件事。

要决定的事通俗解释例子
ResourceAPI 暴露的名词orders, customers, invoices
Operation对名词做什么GET, POST, PATCH, DELETE
SchemaJSON 的形状和限制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 不够。建议固定 typetitlestatusdetail,必要时加字段级 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 兼容性检查完整走一遍。

#claude-code #api #rest #openapi #design
免费

免费 PDF: Claude Code 速查表

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

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

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

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

Masa

关于作者

Masa

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