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

用 Claude Code 做生产级 API 开发:OpenAPI、Next.js、Zod 与 CI

用 Claude Code 构建生产级 API:OpenAPI 契约、Next.js Route Handler、Zod 校验、测试和 CI。

用 Claude Code 做生产级 API 开发:OpenAPI、Next.js、Zod 与 CI

用 Claude Code 写 API 时,最容易犯的错是只让它生成“能跑的接口”。演示环境里这样很快,但生产 API 需要更多边界:契约、输入校验、认证、幂等性、限流、统一错误、日志、测试和 CI。如果这些没有在一开始说清楚,后面每一次改动都会变成补洞。

本文把 Claude Code 当作生产 API 开发助手,而不是简单的代码生成器。做法是 contract-first,也就是先用 OpenAPI 定义接口承诺,再用 Next.js Route Handler 实现,用 Zod 守住输入边界,最后把测试和 GitHub Actions 交给团队继续维护。

Masa 在试做一个订单 API 时,最初只提示“实现 POST /orders”,结果每次生成的错误格式、重试行为和日志字段都不一样。后来把 OpenAPI、认证边界、幂等规则、错误信封和 CI 命令一起写进提示词,审查重点就从“代码像不像”变成了“是否遵守契约”。

先用 OpenAPI 固定 API 契约

OpenAPI 是用 YAML 或 JSON 描述 HTTP API 的标准。它会写清楚路径、方法、请求体、响应、认证方式和错误状态。通俗地说,它是实现之前先写好的 API 约定。规范请参考官方 OpenAPI Specification

openapi: 3.1.0
info:
  title: Orders API
  version: 1.0.0
paths:
  /api/orders:
    post:
      operationId: createOrder
      summary: Create an order
      security:
        - bearerAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
            minLength: 8
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOrder"
      responses:
        "201":
          description: Order created
        "400":
          description: Invalid request
        "401":
          description: Missing or invalid token
        "409":
          description: Idempotency key reused with another payload
        "429":
          description: Rate limit exceeded
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  schemas:
    CreateOrder:
      type: object
      required: [customerId, items, currency]
      properties:
        customerId:
          type: string
          minLength: 3
        currency:
          type: string
          enum: [JPY, USD, EUR]
        items:
          type: array
          minItems: 1

给 Claude Code 的提示词要明确“不能偏离契约”:

把 openapi.yaml 当作契约,实现 Next.js App Router 的 POST /api/orders。
要求:Zod 校验请求体,Authorization: Bearer <token> 在入口处检查,
Idempotency-Key 必填,同 key 同 payload 返回相同响应,
同 key 不同 payload 返回 409,加入 60 秒限流,
错误统一为 { error: { code, message, requestId, details } },
并生成 Vitest 测试和 GitHub Actions CI 检查。

Claude Code 的基础用法可以看官方 Claude Code overview,Next.js 的当前写法请以 Route Handlers 文档为准。

用 Next.js 和 Zod 守住入口边界

入口边界是外部数据第一次进入系统的地方。浏览器、移动端、合作伙伴系统和 webhook 都可能发送缺字段、旧枚举、坏 JSON 或重复请求。Zod 是 TypeScript 的运行时校验库,能把“类型看起来对”和“运行时真的对”连接起来,官方文档见 Zod

下面的代码可以放在 app/api/orders/route.ts。为了方便复制运行,它用内存 Map 保存订单、幂等结果和限流桶。生产环境要把这些换成数据库、Redis 或 API Gateway。

import { z } from "zod";

export const runtime = "nodejs";

const CreateOrderSchema = z.object({
  customerId: z.string().min(3),
  currency: z.enum(["JPY", "USD", "EUR"]),
  items: z.array(z.object({
    sku: z.string().min(1),
    quantity: z.number().int().positive().max(99),
  })).min(1),
  note: z.string().max(500).optional(),
});

type Order = z.infer<typeof CreateOrderSchema> & {
  id: string;
  status: "accepted";
  createdAt: string;
};

const orders = new Map<string, Order>();
const idempotency = new Map<string, { fingerprint: string; status: number; body: unknown }>();
const buckets = new Map<string, { count: number; resetAt: number }>();

export function __resetForTests() {
  orders.clear();
  idempotency.clear();
  buckets.clear();
}

function send(status: number, body: unknown, headers: Record<string, string> = {}) {
  return Response.json(body, { status, headers });
}

function fail(status: number, code: string, message: string, requestId: string, details?: unknown) {
  return send(status, { error: { code, message, requestId, ...(details ? { details } : {}) } });
}

function actor(req: Request) {
  const expected = process.env.API_TOKEN;
  const raw = req.headers.get("authorization") ?? "";
  const token = raw.startsWith("Bearer ") ? raw.slice(7) : "";
  return expected && token === expected ? token.slice(0, 12) : null;
}

function allowed(key: string) {
  const now = Date.now();
  const current = buckets.get(key);
  if (!current || current.resetAt <= now) {
    buckets.set(key, { count: 1, resetAt: now + 60_000 });
    return true;
  }
  if (current.count >= 30) return false;
  current.count += 1;
  return true;
}

export async function POST(req: Request) {
  const requestId = req.headers.get("x-request-id") ?? crypto.randomUUID();
  const who = actor(req);
  if (!who) return fail(401, "unauthorized", "Invalid API token.", requestId);
  if (!allowed(who)) return fail(429, "rate_limited", "Too many requests.", requestId);

  const idempotencyKey = req.headers.get("idempotency-key");
  if (!idempotencyKey || idempotencyKey.length < 8) {
    return fail(400, "missing_idempotency_key", "Idempotency-Key is required.", requestId);
  }

  const rawBody = await req.text();
  const cacheKey = `${who}:${idempotencyKey}`;
  const cached = idempotency.get(cacheKey);
  if (cached && cached.fingerprint !== rawBody) {
    return fail(409, "idempotency_conflict", "Same key was used with another payload.", requestId);
  }
  if (cached) return send(cached.status, cached.body, { "x-idempotent-replay": "true" });

  let payload: unknown;
  try {
    payload = JSON.parse(rawBody);
  } catch {
    return fail(400, "invalid_json", "Request body must be JSON.", requestId);
  }

  const parsed = CreateOrderSchema.safeParse(payload);
  if (!parsed.success) {
    return fail(400, "validation_failed", "Request does not match the contract.", requestId, parsed.error.flatten());
  }

  const order: Order = { ...parsed.data, id: crypto.randomUUID(), status: "accepted", createdAt: new Date().toISOString() };
  orders.set(order.id, order);

  const body = { data: order, meta: { requestId } };
  idempotency.set(cacheKey, { fingerprint: rawBody, status: 201, body });
  console.info("orders.create", { requestId, orderId: order.id, itemCount: order.items.length });
  return send(201, body, { "x-request-id": requestId });
}

统一错误、幂等性、限流和观测性

错误信封是所有失败响应共用的格式。没有统一格式时,前端和合作伙伴很难判断是重试、提示用户,还是联系支持。上面的实现统一返回:

{
  "error": {
    "code": "validation_failed",
    "message": "Request does not match the contract.",
    "requestId": "6f0c9c0f-6db7-4bdf-930b-7cc7d13f3f77",
    "details": {
      "fieldErrors": {
        "items": ["Array must contain at least 1 element(s)"]
      }
    }
  }
}

幂等性表示同一个操作重试时不会产生第二次副作用。订单、支付、邮件发送和积分发放都需要它。限流用于保护系统和免费额度。观测性不是玄学,它只是让你在事故后能用 requestId 找到日志、资源 ID、耗时和错误码。

适用场景至少有三个:B2B SaaS 的订单创建 API、内部审批工具、webhook 接收端,以及带免费额度的公开 API。常见坑也很具体:OpenAPI 和 Zod 字段不一致;同一个幂等 key 搭配不同 payload 仍然放行;把内存限流直接上线;在错误或日志里暴露 token、地址、卡号等敏感信息。

在给 Claude Code 下指令时,最好把这些坑写成验收条件,而不是写成建议。例如“同一个幂等 key 和不同 payload 必须返回 409”、“日志不能包含 Authorization header”、“OpenAPI 的 required 字段必须和 Zod schema 一致”。这样做的好处是,Claude Code 生成测试时会把失败路径也覆盖进去。对团队来说,这比事后在评审里逐条提醒更可靠,因为约束会留在代码、测试和 CI 中。

还有一个容易被忽略的点是本地样例和生产实现的边界。本文为了可复制运行使用内存 Map,但这只适合单进程开发环境。如果你的服务部署在多个实例、Serverless 平台或容器集群中,幂等结果和限流计数必须放到共享存储里。把这句话写进 prompt,可以避免 Claude Code 把教学代码误当成生产架构。

测试和 CI 才是真正的交接

测试不要作为“以后再做”的任务。让 Claude Code 在同一个变更里生成 API 测试,审查时就能围绕行为讨论。下面的 Vitest 直接调用 Route Handler,不需要启动 HTTP 服务器。

import { beforeEach, describe, expect, it } from "vitest";
import { POST, __resetForTests } from "../app/api/orders/route";

const validOrder = {
  customerId: "cus_123",
  currency: "JPY",
  items: [{ sku: "book-001", quantity: 2 }],
};

function req(body: unknown, headers: Record<string, string> = {}) {
  return new Request("http://localhost/api/orders", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization: "Bearer test-token",
      "idempotency-key": crypto.randomUUID(),
      ...headers,
    },
    body: JSON.stringify(body),
  });
}

describe("POST /api/orders", () => {
  beforeEach(() => {
    process.env.API_TOKEN = "test-token";
    __resetForTests();
  });

  it("creates an order", async () => {
    const res = await POST(req(validOrder));
    expect(res.status).toBe(201);
    expect((await res.json()).data.status).toBe("accepted");
  });

  it("rejects invalid input", async () => {
    const res = await POST(req({ ...validOrder, items: [] }));
    expect(res.status).toBe(400);
    expect((await res.json()).error.code).toBe("validation_failed");
  });

  it("returns 409 for conflicting idempotency reuse", async () => {
    const key = "order-key-001";
    await POST(req(validOrder, { "idempotency-key": key }));
    const res = await POST(req({ ...validOrder, currency: "USD" }, { "idempotency-key": key }));
    expect(res.status).toBe(409);
  });
});

CI 的作用是把约定交给仓库,而不是交给人的记忆。GitHub Actions 的语法以官方 Workflow syntax为准。

name: api-contract
on:
  pull_request:
    paths:
      - "app/api/**"
      - "tests/**/*.route.test.ts"
      - "openapi.yaml"
jobs:
  api-contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx @redocly/cli lint openapi.yaml
      - run: npx vitest run tests/**/*.route.test.ts

收益导线和实际结果

如果读者正在做生产 API,他通常不是随便看看,而是担心团队能不能安全落地。可以自然引导到 Claude Code 培训与咨询,或者给个人开发者提供免费速查表。更多测试细节可以继续读API 测试自动化,版本策略可以看API 版本管理

这套流程实际试下来,比单纯提示“写一个接口”更容易审查。尤其是 409 幂等冲突、Zod 的 fieldErrors、带 requestId 的日志和 OpenAPI lint 一起出现时,Claude Code 生成的不是演示代码,而是团队可以继续维护的 API 起点。

#Claude Code #API 开发 #OpenAPI #Next.js #Zod #CI
免费

免费 PDF: Claude Code 速查表

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

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

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

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

Masa

关于作者

Masa

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