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

Claude Code 与 Zod 验证实战指南

用 Claude Code 和 Zod 构建表单、API、环境变量、Webhook 与测试的类型安全验证流程。

Claude Code 与 Zod 验证实战指南

为什么要在运行时使用 Zod

TypeScript 可以在编辑器和编译阶段帮助我们发现类型问题,但它不会自动检查运行中的外部数据。表单提交、API 请求、Webhook payload、process.env、写入数据库前的数据,都可能不是你以为的形状。运行时验证就是在程序执行时确认数据是否可信。Zod 的作用是写一份 schema,也就是数据规则说明,并从这份规则推导出 TypeScript 类型。

Claude Code 很适合辅助 Zod 实装,因为验证逻辑通常由字段、类型、限制、错误信息和使用位置组成。只说“帮我做验证”会得到很泛的代码;明确说“表单、API request/response、环境变量、Webhook payload、DB 写入前都要验证,并附测试”,结果会更接近真实项目。细节可以对照 Zod 官方文档Next.js Route Handler 官方文档

unknown input
  -> Zod schema
  -> safeParse
  -> typed data
  -> business logic
  -> response schema
  -> client

关键是第一步。外部输入先当作 unknown,通过 Zod 后再交给业务逻辑。不要直接写 as SomeType 来假装它已经安全。

先把使用场景交给 Claude Code

不同入口需要不同 schema。表单需要面向用户的错误信息,API 需要稳定的 HTTP 错误,环境变量需要启动时失败,Webhook 还要先做签名验证。

场景入口Zod 要保护的内容
表单浏览器输入空字符串、邮箱格式、长度、同意勾选
API request/responserequest.json() 和响应 JSON非法 payload、响应契约、状态枚举
环境变量process.env缺少密钥、URL 格式、端口范围
Webhook payload第三方 POST事件类型、对象 ID、金额、签名流程
DB 写入前应用内转换后的值可保存字段、枚举、必填 ID

建议把这个表直接放进 Claude Code 提示词。不要让一个 schema 同时覆盖所有层。表单可能有确认密码和勾选框,DB 写入 schema 则更关心 userId、状态和时间戳。可以共享 emailSchemaidSchema 这种小部件,但不要为了复用而复用整棵对象。表单部分可继续看站内的 React Hook Form 指南,API 类型安全可参考 tRPC 开发指南

创建基础 Zod schema

下面用咨询表单作为例子。这个 schema 可以直接复制到 TypeScript 项目中,再按实际字段调整。让 Claude Code 生成时,要说明错误文案语言、字段是否必填、长度限制,以及不要混入数据库字段。

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

export const contactFormSchema = z.object({
  name: z
    .string()
    .trim()
    .min(1, "请输入姓名")
    .max(80, "姓名不能超过 80 个字符"),
  email: z
    .string()
    .trim()
    .email("请输入有效的邮箱地址"),
  plan: z.enum(["trial", "team", "enterprise"]),
  message: z
    .string()
    .trim()
    .min(10, "咨询内容至少需要 10 个字符")
    .max(2000, "咨询内容不能超过 2000 个字符"),
  agreedToPolicy: z
    .boolean()
    .refine((value) => value, "需要同意隐私政策"),
});

export type ContactFormInput = z.infer<typeof contactFormSchema>;

trim() 可以避免只输入空格也通过验证。z.enum 可以把普通字符串限制为固定选项。z.infer 可以从 schema 推导类型,减少 schema 和 interface 两边同时维护的负担。

用 safeParse 处理 API 错误

parse 在失败时会抛出异常,适合环境变量或启动配置这种“失败就停止”的场景。safeParse 会返回成功或失败对象,适合表单和 API,因为你通常要返回 400 和字段级错误。

// src/lib/validation.ts
import { z } from "zod";

export type ValidationProblem = {
  path: string;
  message: string;
};

export function validateInput<TSchema extends z.ZodTypeAny>(
  schema: TSchema,
  input: unknown,
):
  | { ok: true; data: z.infer<TSchema> }
  | { ok: false; status: 400; errors: ValidationProblem[] } {
  const result = schema.safeParse(input);

  if (!result.success) {
    return {
      ok: false,
      status: 400,
      errors: result.error.issues.map((issue) => ({
        path: issue.path.join(".") || "_root",
        message: issue.message,
      })),
    };
  }

  return { ok: true, data: result.data };
}

这个 helper 也方便以后做本地化。小项目可以直接返回 message,多语言产品可以返回 message key,再由前端翻译。

在 Next.js Route Handler 中验证 request 和 response

API 不只要验证输入,也应该验证响应。响应 schema 可以防止内部状态不小心变成前端不认识的字符串。

// app/api/contact/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
import {
  contactFormSchema,
  type ContactFormInput,
} from "@/lib/schemas/contact";
import { validateInput } from "@/lib/validation";

const contactResponseSchema = z.object({
  id: z.string().min(1),
  status: z.enum(["queued"]),
});

async function saveContact(input: ContactFormInput) {
  // Replace this with your database insert.
  return {
    id: `contact_${Date.now()}`,
    status: "queued" as const,
    email: input.email,
  };
}

export async function POST(request: Request) {
  const body: unknown = await request.json();
  const validated = validateInput(contactFormSchema, body);

  if (!validated.ok) {
    return NextResponse.json(
      { message: "请检查输入内容", errors: validated.errors },
      { status: validated.status },
    );
  }

  const saved = await saveContact(validated.data);
  const response = contactResponseSchema.parse(saved);

  return NextResponse.json(response, { status: 201 });
}

Webhook 的顺序要更严格:先验证签名,再验证 payload schema,最后才处理事件。可以让 Claude Code 把 verifySignaturewebhookPayloadSchemahandleWebhookEvent 分成三个函数,便于审查安全边界。

启动时验证环境变量

环境变量本质上是字符串或 undefined。如果 DATABASE_URL 缺失,最好在启动时失败,而不是等到用户请求进来才报错。

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

const envSchema = z.object({
  NODE_ENV: z
    .enum(["development", "test", "production"])
    .default("development"),
  DATABASE_URL: z.string().url("DATABASE_URL must be a valid URL"),
  NEXT_PUBLIC_APP_URL: z.string().url("NEXT_PUBLIC_APP_URL must be a valid URL"),
  WEBHOOK_SECRET: z.string().min(32, "WEBHOOK_SECRET must be at least 32 chars"),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),
});

const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
  console.error(
    "Invalid environment variables",
    parsed.error.flatten().fieldErrors,
  );
  throw new Error("Invalid environment variables");
}

export const env = parsed.data;

z.coerce.number() 很方便,但不要滥用。它适合环境变量和 URL query 这种确定来自字符串的输入。对于普通 JSON body,先确认业务是否真的允许自动转换。

与 react-hook-form 集成

前端验证让用户更快看到错误,后端验证负责真正的安全。两者不能互相替代。

// src/components/contact-form.tsx
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import {
  contactFormSchema,
  type ContactFormInput,
} from "@/lib/schemas/contact";

export function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<ContactFormInput>({
    resolver: zodResolver(contactFormSchema),
    defaultValues: {
      name: "",
      email: "",
      plan: "trial",
      message: "",
      agreedToPolicy: false,
    },
  });

  async function onSubmit(values: ContactFormInput) {
    const response = await fetch("/api/contact", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(values),
    });

    if (!response.ok) {
      throw new Error("Failed to send contact request");
    }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} aria-invalid={Boolean(errors.name)} />
      {errors.name && <p>{errors.name.message}</p>}

      <input {...register("email")} aria-invalid={Boolean(errors.email)} />
      {errors.email && <p>{errors.email.message}</p>}

      <select {...register("plan")}>
        <option value="trial">Trial</option>
        <option value="team">Team</option>
        <option value="enterprise">Enterprise</option>
      </select>

      <textarea {...register("message")} />
      {errors.message && <p>{errors.message.message}</p>}

      <label>
        <input type="checkbox" {...register("agreedToPolicy")} />
        I agree to the privacy policy
      </label>
      {errors.agreedToPolicy && <p>{errors.agreedToPolicy.message}</p>}

      <button type="submit" disabled={isSubmitting}>
        Send
      </button>
    </form>
  );
}

让 Claude Code 修改表单时,要明确要求它不要删除服务器侧的 schema 检查。只靠浏览器验证是常见安全漏洞。

Claude Code 审查提示词

生成后再让 Claude Code 做一次专门审查。提示词要窄,才能得到可执行反馈。

Review only the Zod validation design in these files.

Check:
1. Every external input is treated as unknown until safeParse or parse succeeds.
2. Route Handlers return 400 with field-level errors for invalid requests.
3. Environment variables fail fast at startup.
4. coerce and transform are used only where the input source is clear.
5. Form schemas, API schemas, and database insert schemas are not over-shared.
6. Error messages are user-facing and ready for localization.

Do not refactor unrelated business logic.
Return findings with file paths, risk level, and a minimal patch suggestion.

这个提示词会检查真正容易出问题的地方:TypeScript 类型被过度信任、parsesafeParse 混用、coerce 过度、transform 中包含副作用、错误文案没有本地化计划、schema 被过度共享。

用测试固定契约

schema 是产品契约。至少写一个成功例和几个失败例,避免以后改动时放宽了规则。

// src/lib/schemas/contact.test.ts
import { describe, expect, it } from "vitest";
import { contactFormSchema } from "./contact";

describe("contactFormSchema", () => {
  it("accepts a valid contact request", () => {
    const result = contactFormSchema.safeParse({
      name: "Masa",
      email: "masa@example.com",
      plan: "team",
      message: "I want to introduce Claude Code to my team.",
      agreedToPolicy: true,
    });

    expect(result.success).toBe(true);
  });

  it("rejects invalid email and short message", () => {
    const result = contactFormSchema.safeParse({
      name: "Masa",
      email: "not-an-email",
      plan: "team",
      message: "short",
      agreedToPolicy: true,
    });

    expect(result.success).toBe(false);
    if (!result.success) {
      expect(result.error.issues.map((issue) => issue.path.join("."))).toEqual(
        expect.arrayContaining(["email", "message"]),
      );
    }
  });
});

DB 写入前也建议写测试,使用最终准备保存的对象来验证。这样字段名、枚举值或转换逻辑变化时,可以及时发现。

常见陷阱

第一,只写 TypeScript 类型不等于运行时验证。request.json() as ContactFormInput 只是告诉编译器相信你,并没有证明 payload 合法。

第二,parsesafeParse 要按场景使用。用户输入和 API 请求通常用 safeParse,环境变量和启动配置可以失败即停止。

第三,谨慎使用 coerce。它会把字符串转换成数字或布尔值,适合明确的字符串来源,不适合所有输入。

第四,不要在 transform 里做副作用。数据库写入、发邮件、打点日志都应该放在验证之后。

第五,错误文案和本地化不要最后才想。多语言产品最好返回 message key,或者在 API 层统一转换 Zod issue。

第六,不要过度复用 schema。表单、API、Webhook、DB 插入可能相似,但不是同一个契约。

咨询与验证记录

如果你的项目已经有很多表单、Webhook 或环境变量,Claude Code Lab 的咨询与训练可以帮助你整理 schema 分层、审查提示词、测试策略和团队规则。

本文示例在 2026-06-02 依据 Zod 与 Next.js Route Handler 官方文档整理。代码假设项目已安装 zodreact-hook-form@hookform/resolversvitest。实际项目中,Masa 会再补上认证、CSRF 或 Webhook 签名验证、数据库约束,以及每个外部入口的失败测试。

#Claude Code #Zod #validation #TypeScript #type safety
免费

免费 PDF: Claude Code 速查表

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

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

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

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

Masa

关于作者

Masa

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