以前写后台接口,最让人抓狂的就是在业务逻辑开头写那一坨又一坨的入参校验代码。什么 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); } };};
// 业务调用示范:创建用户订单的接口校验 Schemaexport 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 应用,这种零星开销换来极致的安全性和可维护性,性价比简直高到爆。