Tips & Tricks

用 Claude Code 极速设计、实现和测试 REST API | 从 OpenAPI 规范到生产环境

学习如何用 Claude Code 端到端开发 REST API:从 OpenAPI 规范生成到生产就绪的 TypeScript 代码,包含 Hono、zod 验证、vitest 测试自动生成及完整可运行代码示例。

为什么 Claude Code 特别适合 REST API 设计

REST API 开发充满了重复性模式:端点定义、数据校验、错误处理、测试——全部需要手动编写。在处理这些样板代码的同时,很难专注于真正重要的业务逻辑。

我是 Masa,一位负责内部系统 API 现代化的 DX 工程师。引入 Claude Code 之前,我每个端点的标准工时是”设计 30 分钟 → 实现 2 小时 → 测试 1 小时”。全面集成 Claude Code 后缩短为设计 10 分钟 → 实现 30 分钟 → 测试 15 分钟

Claude Code 在 API 开发上的 3 大优势

  1. 将 OpenAPI 规范作为上下文理解 — 粘贴 YAML 即可立即生成符合规范的代码
  2. 深度掌握 TypeScript 类型系统 — 从一开始就生成类型安全的代码
  3. 熟知大量测试模式 — 一次生成正常路径、错误路径和边界情况测试

1. 用 Claude Code 自动生成 OpenAPI 规范

提示词示例

请根据以下需求生成 OpenAPI 3.1 规范(YAML)。

资源:Task(任务管理)
字段:
  - id:UUID
  - title:string(1–200 字符)
  - description:string(可选)
  - status:"todo" | "in_progress" | "done"
  - priority:"low" | "medium" | "high"(默认:medium)
  - assignee_id:UUID(可选)
  - due_date:ISO8601 格式(可选)
  - created_at:ISO8601 格式
  - updated_at:ISO8601 格式

端点:
  - GET /tasks(带分页的列表)
  - GET /tasks/:id
  - POST /tasks
  - PUT /tasks/:id(完整更新)
  - PATCH /tasks/:id(部分更新)
  - DELETE /tasks/:id

响应格式:{ data, meta, errors }
错误码:400, 401, 403, 404, 422, 500
认证:Bearer Token(Authorization 请求头)

生成的 OpenAPI YAML(节选)

openapi: 3.1.0
info:
  title: Task Management API
  version: 1.0.0
  description: 任务管理 API 的 OpenAPI 规范

servers:
  - url: https://api.example.com/v1
    description: 生产环境
  - url: http://localhost:3000/v1
    description: 开发环境

components:
  schemas:
    Task:
      type: object
      required: [id, title, status, priority, created_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
          minLength: 1
          maxLength: 200
        status:
          type: string
          enum: [todo, in_progress, done]
        priority:
          type: string
          enum: [low, medium, high]
          default: medium

2. 用 Hono 实现类型安全的处理器

// src/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { prettyJSON } from "hono/pretty-json";
import { taskRouter } from "./routes/tasks";

const app = new Hono();

// 中间件
app.use("*", cors());
app.use("*", logger());
app.use("*", prettyJSON());

// 路由注册
app.route("/v1/tasks", taskRouter);

// 全局错误处理器
app.onError((err, c) => {
  console.error(err);
  return c.json(
    { data: null, meta: null, errors: [{ code: "INTERNAL_ERROR", message: "服务器发生内部错误" }] },
    500
  );
});

export default app;

任务路由器实现

// src/routes/tasks.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { TaskCreateSchema, TaskUpdateSchema, TaskPatchSchema, TaskQuerySchema } from "../schemas/task";
import { TaskService } from "../services/taskService";

export const taskRouter = new Hono();
const taskService = new TaskService();

// 获取任务列表
taskRouter.get("/", zValidator("query", TaskQuerySchema), async (c) => {
  const query = c.req.valid("query");
  const result = await taskService.findAll(query);
  return c.json({
    data: result.tasks,
    meta: { total: result.total, page: query.page, per_page: query.per_page },
    errors: null,
  });
});

// 按 ID 获取任务
taskRouter.get("/:id", async (c) => {
  const id = c.req.param("id");
  const task = await taskService.findById(id);
  if (!task) {
    return c.json(
      { data: null, meta: null, errors: [{ code: "NOT_FOUND", message: "任务不存在", field: "id" }] },
      404
    );
  }
  return c.json({ data: task, meta: null, errors: null });
});

// 创建任务
taskRouter.post("/", zValidator("json", TaskCreateSchema), async (c) => {
  const body = c.req.valid("json");
  const task = await taskService.create(body);
  return c.json({ data: task, meta: null, errors: null }, 201);
});

// 删除任务
taskRouter.delete("/:id", async (c) => {
  const id = c.req.param("id");
  const deleted = await taskService.delete(id);
  if (!deleted) {
    return c.json(
      { data: null, meta: null, errors: [{ code: "NOT_FOUND", message: "任务不存在", field: "id" }] },
      404
    );
  }
  return c.json({ data: null, meta: null, errors: null }, 204);
});

3. 自动生成 Zod 验证 Schema

// src/schemas/task.ts
import { z } from "zod";

const TaskStatus = z.enum(["todo", "in_progress", "done"]);
const TaskPriority = z.enum(["low", "medium", "high"]);

export const TaskSchema = z.object({
  id: z.string().uuid(),
  title: z.string().min(1, "标题至少需要 1 个字符").max(200, "标题不能超过 200 个字符"),
  description: z.string().nullable(),
  status: TaskStatus,
  priority: TaskPriority,
  assignee_id: z.string().uuid().nullable(),
  due_date: z.string().datetime().nullable(),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
});

export const TaskCreateSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().optional(),
  status: TaskStatus.default("todo"),
  priority: TaskPriority.default("medium"),
  assignee_id: z.string().uuid().optional(),
  due_date: z.string().datetime().optional(),
});

export const TaskUpdateSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().nullable(),
  status: TaskStatus,
  priority: TaskPriority,
  assignee_id: z.string().uuid().nullable(),
  due_date: z.string().datetime().nullable(),
});

export const TaskPatchSchema = TaskUpdateSchema.partial();

export const TaskQuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  per_page: z.coerce.number().int().min(1).max(100).default(20),
  status: TaskStatus.optional(),
  priority: TaskPriority.optional(),
});

export type Task = z.infer<typeof TaskSchema>;
export type TaskCreate = z.infer<typeof TaskCreateSchema>;
export type TaskQuery = z.infer<typeof TaskQuerySchema>;

引入前后对比

指标引入 Claude Code 前引入 Claude Code 后
Schema 定义工时每个端点 30–45 分钟5–10 分钟(含提示词编写)
类型错误发生率项目后期频繁出现从一开始就几乎为零
遗漏的验证情况每次代码评审都被指出边界情况自动覆盖
规范/代码偏差2–3 周后就过时始终同步(从规范生成)

4. 用 Vitest 自动生成测试代码

// src/routes/tasks.test.ts
import { describe, it, expect, vi } from "vitest";
import app from "../index";
import { TaskService } from "../services/taskService";

vi.mock("../services/taskService");

const mockTask = {
  id: "550e8400-e29b-41d4-a716-446655440000",
  title: "测试任务",
  description: "用于测试的任务",
  status: "todo" as const,
  priority: "medium" as const,
  assignee_id: null,
  due_date: null,
  created_at: "2026-04-25T00:00:00.000Z",
  updated_at: "2026-04-25T00:00:00.000Z",
};

describe("GET /v1/tasks", () => {
  it("正常情况:返回任务列表", async () => {
    vi.mocked(TaskService.prototype.findAll).mockResolvedValue({ tasks: [mockTask], total: 1 });
    const res = await app.request("/v1/tasks", { headers: { Authorization: "Bearer valid-token" } });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.data).toHaveLength(1);
  });

  it("异常情况:per_page 超过最大值", async () => {
    const res = await app.request("/v1/tasks?per_page=101", { headers: { Authorization: "Bearer valid-token" } });
    expect(res.status).toBe(400);
  });
});

describe("POST /v1/tasks", () => {
  it("正常情况:创建任务", async () => {
    vi.mocked(TaskService.prototype.create).mockResolvedValue(mockTask);
    const res = await app.request("/v1/tasks", {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: "Bearer valid-token" },
      body: JSON.stringify({ title: "新任务" }),
    });
    expect(res.status).toBe(201);
  });

  it("异常情况:标题为空字符串", async () => {
    const res = await app.request("/v1/tasks", {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: "Bearer valid-token" },
      body: JSON.stringify({ title: "" }),
    });
    expect(res.status).toBe(400);
  });
});

5. 错误处理模式

// src/lib/errors.ts
export class AppError extends Error {
  constructor(
    public readonly code: string,
    public readonly message: string,
    public readonly statusCode: number,
    public readonly field?: string
  ) {
    super(message);
    this.name = "AppError";
  }
}

export const Errors = {
  notFound: (resource: string) => new AppError("NOT_FOUND", `${resource}不存在`, 404),
  unauthorized: () => new AppError("UNAUTHORIZED", "需要身份验证", 401),
  forbidden: () => new AppError("FORBIDDEN", "您没有权限访问此资源", 403),
  conflict: (message: string) => new AppError("CONFLICT", message, 409),
  internal: () => new AppError("INTERNAL_ERROR", "服务器发生内部错误", 500),
};

3 个常见陷阱

陷阱 1:缺少 UUID 验证

Claude Code 有时将路径参数 :id 视为普通字符串。没有 UUID 格式验证,格式错误的查询可能到达数据库。

// 错误:将原始字符串直接发送到数据库
const task = await db.findById(c.req.param("id"));

// 正确:先验证 UUID 格式
const idSchema = z.string().uuid("不是有效的 UUID 格式");
const parsed = idSchema.safeParse(c.req.param("id"));
if (!parsed.success) {
  return c.json({ data: null, meta: null, errors: [{ code: "INVALID_ID", message: "ID 格式无效" }] }, 400);
}
const task = await db.findById(parsed.data);

陷阱 2:缺少 Content-Type 请求头验证

对于没有 Content-Type: application/json 的 PUT/POST 请求,Hono 将 body 视为空对象。zod 验证通过后,空数据会被保存。

// src/middleware/contentType.ts
import { createMiddleware } from "hono/factory";

export const requireJson = createMiddleware(async (c, next) => {
  if (["POST", "PUT", "PATCH"].includes(c.req.method)) {
    const contentType = c.req.header("Content-Type");
    if (!contentType?.includes("application/json")) {
      return c.json(
        { data: null, meta: null, errors: [{ code: "UNSUPPORTED_MEDIA_TYPE", message: "需要 Content-Type: application/json" }] },
        415
      );
    }
  }
  await next();
});

陷阱 3:日期字段的时区问题

z.string().datetime() 只接受 UTC 格式。如果前端发送 2026-04-25T17:00:00+08:00,会被拒绝。

// 错误
due_date: z.string().datetime()

// 正确:允许时区偏移
due_date: z.string().datetime({ offset: true }).optional()

总结

将 Claude Code 集成到 REST API 开发中,可以显著缩短上线时间:

  1. 首先生成 OpenAPI 规范 — 它成为所有后续步骤的基础
  2. 从规范生成 Zod Schema — 手动编写会导致规范与代码不一致
  3. 明确要求正常路径、错误路径和边界情况测试 — 不加指示只会写正常路径
  4. 在提示词中预先包含常见陷阱 — UUID 验证、Content-Type 检查、时区处理

采用这一工作流后,每个端点的开发时间减少了约 70%

话虽如此,切勿在未经审查的情况下将 Claude Code 的输出部署到生产环境。安全性(SQL 注入防护、输入净化)和业务逻辑(定价、库存管理等)始终需要人工审查。Claude Code 是快速生成初稿的工具,最终的质量责任在开发者。


相关文章:

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

让你的 Claude Code 工作流更上一层楼

50 个经过实战检验的提示词模板,现在就能复制粘贴到 Claude Code 中使用。

免费

免费 PDF:5 分钟看懂 Claude Code 速查表

只需留下邮箱,我们就会立即把这份 A4 一页速查表 PDF 发送给你。

我们会严格保护你的个人信息,绝不发送垃圾邮件。

Masa

本文作者

Masa

深度使用 Claude Code 的工程师。运营 claudecode-lab.com——一个涵盖 10 种语言、超过 2,000 页内容的科技媒体。