参数验证实战:提升API安全与数据可靠性的关键技巧
为什么我又在折腾参数验证?
最近重构一个老项目,发现接口里传的参数五花八门,有的字段明明是数字,前端传了个字符串;有的必填字段直接没传,后端也没拦住,结果数据库里一堆 null。我一查日志,好家伙,光上个月就因为参数问题导致了 3 次线上异常。所以这次我决定把参数验证这块彻底理清楚——不是简单加个 if 判断就完事,而是选一套靠谱、可维护、不拖累开发效率的方案。
我试过几种主流做法:纯手写校验逻辑、用 Yup、Zod,还有 TypeScript 的运行时校验(比如 io-ts)。今天就来唠唠这些方案在真实项目里的表现,哪些让我省心,哪些让我半夜想删库跑路。
手写校验:灵活但累成狗
最原始的办法,就是每个接口入口处写一堆 if-else:
function createUser(data) {
if (!data.name || typeof data.name !== 'string') {
throw new Error('name is required and must be string');
}
if (typeof data.age !== 'number' || data.age < 0) {
throw new Error('age must be a non-negative number');
}
// ... 继续写下去
}
优点?当然有——完全掌控,想怎么校验就怎么校验,连“手机号必须以 13 开头”这种奇葩需求都能塞进去。但缺点更明显:重复代码爆炸,改一个字段要改 N 个地方,而且容易漏。我之前在一个表单里漏了邮箱格式校验,结果用户注册了一堆 abc@ 这种垃圾数据,清理起来头大。
除非是极简脚本或临时 demo,否则我真不推荐。你的时间不值钱吗?
Yup:老牌选手,但有点“重”
Yup 是我早期用得最多的方案,尤其配合 Formik 做表单验证简直标配。定义 schema 很直观:
import * as yup from 'yup';
const userSchema = yup.object({
name: yup.string().required(),
age: yup.number().min(0).required(),
email: yup.string().email().required()
});
// 使用
try {
await userSchema.validate(data, { abortEarly: false });
} catch (err) {
console.log(err.errors); // 所有错误信息
}
Yup 的优势在于生态成熟,社区插件多,异步校验也支持(比如检查用户名是否已存在)。但问题也不少:
- 体积不小,gzip 后还有 10KB+,对轻量项目不太友好
- 类型推导弱,TypeScript 里得额外加
@types/yup,而且不能自动从 schema 推出 interface,得手动同步 - API 有点啰嗦,
.string().required()写多了手酸
我去年一个中后台项目用 Yup,结果打包体积被它拖慢了 0.5s,老板问起来我还得解释半天。现在除非是已有 Formik 项目,否则我基本不碰它了。
Zod:真香!我现在的首选
自从用了 Zod,我感觉自己以前在参数验证上浪费了太多生命。它和 TypeScript 集成得近乎完美——你定义 schema,它自动生成类型,反过来也行:
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(1),
age: z.number().min(0),
email: z.string().email()
});
type User = z.infer<typeof UserSchema>; // 自动推导出 TS 类型!
// 校验
const result = UserSchema.safeParse(data);
if (!result.success) {
console.log(result.error.flatten()); // 结构化错误
return;
}
// result.data 就是类型安全的 User 对象
我特别喜欢它的几点:
- 零依赖,打包后只有 5KB 左右
- 类型推导丝滑,改 schema 就等于改类型,不用两边维护
- API 简洁,链式调用很舒服,还支持 transform、default 等高级操作
- 错误信息结构清晰,前端直接拿来展示都行
上周我重构一个 API 层,把所有手写校验换成 Zod,代码量少了 40%,还顺手修复了两个隐藏的类型漏洞。唯一的小坑是:它的 .parse() 会抛异常,得用 .safeParse() 才能静默处理——我一开始不知道,测试时直接崩了,折腾了半天才反应过来。
TypeScript 运行时校验(io-ts):强大但学习成本高
io-ts 是另一种思路:用 TypeScript 的类型系统反向生成运行时校验。听起来很酷,但实际用起来有点绕:
import * as t from 'io-ts';
const User = t.type({
name: t.string,
age: t.number,
email: t.string
});
type User = t.TypeOf<typeof User>;
// 校验
const result = User.decode(data);
if (result._tag === 'Left') {
console.log(result.left); // 错误详情
}
它的优势是理论完备性高,适合超大型系统或对类型安全有极致要求的场景(比如金融)。但缺点也很致命:
- API 不直观,
.decode()返回 Either 类型,处理起来啰嗦 - 文档晦涩,新手容易懵
- 生态不如 Zod 活跃,社区方案少
我试过在一个内部工具里用 io-ts,结果团队新人看了直摇头,说“这比写正则还难”。除非你团队全是 FP 爱好者,否则真没必要硬上。
我的选型逻辑:看场景,别死磕
总结一下我的实战选择:
- 新项目(TS + React/Vue):无脑选 Zod。类型安全、轻量、API 友好,三者兼得,何乐不为?
- 老项目(JS + Formik):继续用 Yup,迁移成本太高,不如留着。
- 超简单脚本或临时接口:手写几行 if 也行,别为了校验引入一整套库。
- 金融/医疗等强合规场景:可以考虑 io-ts,但务必评估团队接受度。
另外提醒一点:参数验证只是防线的第一环。再好的 schema 也防不住恶意攻击,记得配合 CSP、CORS、速率限制等安全措施。我见过有人以为用了 Zod 就万事大吉,结果被 SQL 注入搞穿了——schema 只管格式,不管内容安全啊!
最后说两句
参数验证这事,看似简单,实则影响整个系统的健壮性。我踩过坑,也试过各种方案,现在基本固定用 Zod 了。它不是银弹,但在我经手的 80% 项目里都是最优解。
以上是我个人对参数验证方案的对比总结,有更优的实现方式欢迎评论区交流。如果你也在用 Zod,不妨分享下你的踩坑经验?

暂无评论