Zod:TypeScript 项目的最后一道防线
你写的 API 路由接收了一个 POST 请求,req.body 是用户传来的数据。它包含 title、category、tags,但它是否真的符合你的数据库约束?
90% 的 Node.js 项目靠手动 if 检查解决这个问题。剩下的 10% 用 Zod。
Zod(43,247 个 GitHub stars,MIT 协议)是一个 TypeScript 优先的 Schema 验证库。你定义一个 schema,用一行 .parse() 验证数据——验证通过后返回一个带完整类型推断的结果,而不是 any。
三个核心亮点
1. 一个 Schema 同时做两件事:验证 + 类型
传统做法是定义 TypeScript interface,再写一套运行时校验——两套定义,两处维护,早晚不一致。
Zod 把这件事合并了:
import { z } from 'zod';
const Article = z.object({
title: z.string().min(1, '标题不能为空').max(300),
category: z.enum(['AI', 'Apple', '科技前沿']),
tags: z.array(z.string().max(50)).max(10).default([]),
status: z.enum(['published', 'draft', 'archived']).default('published'),
});
// typeof ArticleInput 自动推断为:
// { title: string; category: 'AI' | 'Apple' | '科技前沿'; tags?: string[]; status?: 'published' | 'draft' | 'archived' }
type ArticleInput = z.infer<typeof Article>;
定义 schema 的同时就得到了 TypeScript 类型。没有 interface,没有 type 别名,没有不一致的风险。.infer 是 Zod 最被低估的功能——它让你的类型定义和运行时校验永远同步。
2. .safeParse() 不抛异常
parse() 验证失败时抛出 ZodError,在 Express 中间件里需要 try/catch。.safeParse() 返回一个 discriminated union:
const result = articleCreateSchema.safeParse(req.body);
if (!result.success) {
// result.error 是 ZodError,包含所有字段的验证失败信息
return res.status(400).json({ errors: result.error.issues });
}
// result.data 是已校验的类型安全数据
const { title, category, tags } = result.data;
.success 就是类型守卫。if 分支一过,TypeScript 自动缩窄 result.data 的类型。不需要类型断言,不需要 as。
3. 零依赖,2KB 核心
Zod 的 gzip 后核心包只有 2KB,零外部依赖。可以跑在 Node.js、浏览器、Deno、Bun 任何 JavaScript 运行时。这个大小意味着它适合任何场景——哪怕是微信小程序这种对包大小敏感的环境。
白物集实战:一个完整的 API 请求验证
白物集后端 Express API 每天接受来自多个渠道的内容入库——agent cron 写的早报、热文采集脚本批量的知识卡片、以及手动管理后台的编辑操作。每个入口的数据格式都有细微差异,但都必须符合同一个数据库约束。
我们在 api/src/lib/validation.js 里用 Zod 定义了全套 schema。以文章入库为例:
export const articleCreateSchema = z.object({
title: z.string().min(1, '标题不能为空').max(300, '标题最长300字'),
content_md: z.string().min(1, '内容不能为空'),
content_html: z.string().optional().default(''),
abstract: z.string().max(500).optional().default(''),
category: z.enum(ALL_CATEGORIES, {
errorMap: () => ({ message: `无效分类, 可选: ${ALL_CATEGORIES.join(', ')}` }),
}),
tags: z.array(z.string().max(50)).max(10).optional().default([]),
source: z.string().max(100).optional().default('manual'),
source_url: z.string().url('来源URL格式不正确').or(z.literal('')).optional().default(''),
author: z.string().max(100).optional().default('白物集'),
status: z.enum(['published', 'draft', 'archived']).optional().default('published'),
});
这个 schema 覆盖了 9 个字段,每个字段都有自己的类型、长度限制、默认值,以及自定义错误信息。如果用传统手动校验,这段代码至少 60 行——而且容易漏掉边界情况。
在 ingest 路由里使用它:
const parsed = articleCreateSchema.safeParse(req.body);
if (!parsed.success) return zodError(res, parsed.error);
const { title, content_md, content_html, abstract, category, tags, source, source_url, author, status } = parsed.data;
parsed.data 的九个字段全部有完整类型。编辑器自动补全、编译时类型检查、运行时验证——一次定义,三层保障。
踩坑记录:Zod v4 的 .issues 陷阱
2026 年 7 月,白物集 API 突然返回 500,错误是 Cannot read properties of undefined (reading 'map')。
排查发现,我们的 errors.js 用了 zodErr.errors.map() 来格式化错误信息。这是 Zod v3 的 API。Zod v4 把 .errors 改成了 .issues——两者都是数组,但名字变了。zodErr.errors 返回 undefined,undefined.map() 直接崩溃。
修复很简单:zodErr.errors.map( 改为 (zodErr.issues || []).map(。
这不是 Zod 的错——3.x → 4.x 的主版本号变更本身就应该预期 breaking change。但这个细节提醒我们:版本升级后,你依赖的错误处理路径也要测试。
适合的场景
任何需要接收外部数据的 Node.js 服务。 具体来说:
- API 路由验证:Express 或 Fastify 的 request body 校验。一个 schema 覆盖所有输入字段的类型、长度、格式,比手写 if 链可靠 10 倍。
- 配置文件校验:env 变量、yaml 配置、JSON 配置文件。Zod 的
.parse()会在启动时校验所有配置,让错误在部署前暴露。 - 表单数据验证:前端表单提交前的客户端校验。Zod 可以前后端共用同一个 schema。
- 数据库记录格式保证:从 Supabase 或 Postgres 读取的数据,用 Zod 再确认一次再传给前端。
不适合的场景:
- 已经有完善 OpenAPI 规范 + 自动化校验(如 Fastify 的 schema 验证)的项目。Zod 和 Fastify 内置验证部分重叠。
- 纯 JavaScript 项目(无 TypeScript)。Zod 的 .infer 类型推断是最大优势,纯 JS 用不到这部分。
快速上手
第一步:安装
npm install zod
第二步:定义一个 schema
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string().min(2).max(50),
email: z.string().email(),
role: z.enum(['admin', 'user', 'viewer']),
createdAt: z.string().datetime(),
});
第三步:验证数据
const input = { id: 1, name: '白物集', email: 'hello@baiwuji.top', role: 'admin', createdAt: '2026-07-13T00:00:00Z' };
const user = UserSchema.parse(input);
验证通过,user 的类型是 { id: number; name: string; email: string; role: 'admin' | 'user' | 'viewer'; createdAt: string }。
第四步:在 Express 路由里使用
router.post('/users', (req, res) => {
const result = UserSchema.safeParse(req.body);
if (!result.success) return res.status(400).json({ errors: result.error.issues });
// result.data 的类型安全
res.json({ user: result.data });
});
第五步:用 .infer 导出类型
type User = z.infer<typeof UserSchema>;
// 等价于手动写 interface,但永远与 schema 同步
总结
Zod 解决了一个简单但重要的问题:让进入你程序的每一条数据都经过验证,而且不增加维护成本。
它的设计哲学很克制——只做一件事(Schema 验证 + 类型推断),但做得足够好。2KB 的包体积,零依赖,前后端通用,生态里有 Zod to OpenAPI、Zod to JSON Schema 等配套工具。
白物集从第一天就在 API 层使用 Zod。9 个 route 文件,每个都依赖 validation.js 里的 schema 定义。迄今为止,因为校验逻辑本身出错导致的数据问题为零。
如果你的 TypeScript 项目还没有上 Zod,现在就是最好的时机。