以前写后台接口,最让人抓狂的就是在业务逻辑开头写那一坨又一坨的入参校验代码。什么 if (!req.body.username)、if (typeof req.body.age !== 'number')。稍微复杂一点的嵌套对象或者数组,手写校验逻辑能写到你想吐。更惨的是,前端传过来的 query 参数全是字符串,你还得手动转换 parseInt 或者 parseFloat,一不小心漏掉哪一步,直接导致后续业务代码抛出隐式空指针或者数据库类型报错,调试起来简直是地狱。
最致命的是,这种手写的校验逻辑跟 TypeScript 类型完全是脱节的。就算你在函数入参写了 interface UserDto,运行时的 JS 代码依然是裸奔状态。如果入参不干净,恶意请求可以直接带进各种奇奇怪怪的字段,直接污染后面的内存对象。
为了结束这种野路子校验,我们最近在后端所有的 Express/Koa 路由中全面引入了 Zod。Zod 不仅能在运行时帮你做好防线,做数据强清洗(Strip 掉没有声明的脏字段),最爽的是它能直接推导出 TypeScript 的静态类型,真正实现“定义即校验,校验即类型”。
以下是我们针对 Express 框架封装的通用 Zod 校验中间件。它支持同时校验 body、query 和 params,并且校验通过后,会自动把强类型绑定到 Express 的 Request 范型上:
import { Request, Response, NextFunction } from 'express';
import { AnyZodObject, ZodError, z } from 'zod';
// 定义一个通用的请求校验 Schema 结构
interface RequestValidators {
body?: AnyZodObject;
query?: AnyZodObject;
params?: AnyZodObject;
}
export const validateRequest = (validators: RequestValidators) => {
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
if (validators.params) {
req.params = await validators.params.parseAsync(req.params);
}
if (validators.query) {
req.query = await validators.query.parseAsync(req.query);
}
if (validators.body) {
req.body = await validators.body.parseAsync(req.body);
}
next();
} catch (error) {
if (error instanceof ZodError) {
res.status(400).json({
success: false,
message: 'Validation failed',
errors: error.errors.map((err) => ({
field: err.path.join('.'),
message: err.message,
})),
});
return;
}
next(error);
}
};
};
// 业务调用示范:创建用户订单的接口校验 Schema
export const CreateOrderSchema = {
body: z.object({
productId: z.string().uuid({ message: '产品 ID 格式不正确' }),
quantity: z.number().int().positive({ message: '数量必须是正整数' }),
couponCode: z.string().trim().toUpperCase().optional(),
meta: z.object({
notes: z.string().max(200, { message: '备注不能超过 200 字' }).optional(),
giftWrap: z.boolean().default(false),
}),
}),
query: z.object({
draft: z.enum(['true', 'false']).transform((val) => val === 'true'),
}),
};
使用 Zod 的 transform 函数非常舒服。你看上面的 draft 字段,前端传过来的其实是个字面量字符串,我们直接在 Zod Schema 里面链式调用了 transform,将其安全地转换成了正儿八经的 boolean 类型。当代码走到 Controller 的时候,req.query.draft 已经是一个干净的 boolean 值了,不需要我们在业务层再去做烦人的类型转换。
另外,Zod 默认的 strip 机制也是个神器。如果前端不怀好意地多传了一些 Schema 之外的属性(比如试图修改 is_admin 字段),Zod 的 parseAsync 会直接把这部分多余属性砍掉,只保留 Schema 中定义好的字段。这种干净的输入流,让后面的安全防护心智负担骤降。
接入这套方案之后,团队的后端代码整洁度提升了一大截。以往冗长无聊的防御性代码缩减了 90% 以上。
不过用 Zod 也有个小细节要注意,就是在高并发的边缘计算节点上(比如 Cloudflare Workers),如果你的 Schema 极其庞大而且有复杂的递归定义,parse 可能会有轻微的性能损耗,但对于常规的 Web 应用,这种零星开销换来极致的安全性和可维护性,性价比简直高到爆。