Fastify 最佳实践 #4: 用 Zod 替代 Ajv 管理 Schema
在构建服务端应用时,对传入的 HTTP 请求参数进行校验是保障服务稳定性和安全性的第一道防线。Fastify 作为一款以性能和低开销著称的框架,其内置了强大的 Ajv 模块来高效处理 Schema 验证,真正做到了开箱即用。
然而,在大量 TypeScript 项目的实践中,我发现原生的 Ajv 方案并非完美。对于追求极致开发体验和类型安全的我们来说,它存在两个难以忽视的痛点。
Ajv 的痛点:繁琐与割裂
让我们来看一个使用原生 Ajv 的典型校验示例:
import { FastifyInstance } from 'fastify';
// 使用 JSON Schema 规范定义
const paramsSchema = {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
},
required: ['id'],
additionalProperties: false,
};
// 我们还需要手动为 req.params 定义类型
interface IParams {
id: string;
}
const routes = async (fastify: FastifyInstance) => {
fastify.get<{ Params: IParams }>( // 手动关联类型
'/:id',
{
schema: {
params: paramsSchema,
},
},
async (req, reply) => {
const { id } = req.params;
// ...
return reply.status(200).send();
},
);
};
export default routes;
这种方式虽然能完成任务,但缺点也显而易见:
- 定义繁琐冗长:
Ajv的 JSON Schema 语法相对啰嗦。当校验规则复杂、字段繁多时,Schema 对象会变得异常庞大,难以阅读和维护。 - 类型系统割裂:
Ajv的 Schema 定义和 TypeScript 的类型系统是完全独立的。这意味着你为了运行时校验定义了一遍 Schema,却还需要再手动声明一次对应的 TypeScriptinterface或type来获得编译时类型提示,这无疑增加了重复工作,也带来了两者不一致的风险。虽然可以借助json-schema-to-ts这类工具从 Schema 生成类型,但这又引入了额外的依赖和构建步骤。
那么,有没有一种更现代、对 TypeScript 更友好的方式呢?答案是肯定的:Zod。
解决方案:拥抱 Zod,实现类型与校验的统一
Zod 是一个以 TypeScript 为先的校验库,它允许你只定义一次 Schema,就能同时获得强大的运行时校验和静态类型推断。通过一个“胶水”依赖 fastify-type-provider-zod,我们可以让 Fastify 和 Zod 完美协作。
首先,安装依赖:
npm i fastify-type-provider-zod
然后,我们可以这样重构代码:
import Fastify from 'fastify';
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from 'fastify-type-provider-zod';
import { z } from 'zod';
const app = Fastify({ logger: true });
// 1. 设置 Fastify 使用 Zod 作为校验和序列化工具
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
// 2. 注册路由时,通过 withTypeProvider<ZodTypeProvider>() 启用 Zod 类型提供
app.withTypeProvider<ZodTypeProvider>().route({
method: 'GET',
url: '/',
// 3. 直接使用 Zod 的链式 API 定义 Schema,清晰直观
schema: {
querystring: z.object({
name: z.string().min(4).default('Guest'),
}),
response: {
200: z.object({
greeting: z.string(),
}),
},
},
handler: (req, res) => {
// 4. ✨ 见证奇迹的时刻!✨
// req.query 被自动推断为 { name: string } 类型
// 无需手动定义任何 interface 或 type
const { name } = req.query;
// 你可以放心地使用 name 的所有 string 方法,IDE 的代码补全也会生效
const greeting = `Hello, ${name.toUpperCase()}!`;
// 响应体也会根据 response schema 进行校验和序列化
res.status(200).send({ greeting });
},
});
app.listen({ port: 3000 });
关键改动解析
让我们来解析一下这段代码中的魔法:
app.setValidatorCompiler(validatorCompiler);和app.setSerializerCompiler(serializerCompiler);:这两行是核心配置。它们的作用是告诉 Fastify:“别再用你内置的 Ajv 了,请改用 Zod 来校验(validate)传入的请求,并序列化(serialize)发出的响应。”app.withTypeProvider<ZodTypeProvider>():这个神奇的方法会为你的路由注入 Zod 的类型能力。正是因为它,Fastify 才能理解你在schema字段中写的 Zod 对象,并为你提供完美的类型推断。从这一刻起,你的handler中的req对象(包括query,body,params)都拥有了精确的类型。
定义 response Schema 的额外好处
你可能已经注意到,在上面的例子中我们还定义了 response 的 Schema。这不仅仅是为了文档清晰或确保输出格式正确,它还有一个重要的性能优势:Fastify 会使用这个 Schema 来生成一个优化的 JSON 序列化函数,其速度比标准的 JSON.stringify 快上不少。对于高并发场景,这是一个不容忽视的性能提升点。
总结
从 Ajv 迁移到 Zod,我们得到的远不止是更简洁的语法。我们获得的是:
- 单一事实来源(Single Source of Truth):一份 Schema 定义,同时服务于运行时校验和编译时类型检查。告别重复定义和类型不同步的烦恼。
- 卓越的开发体验(DX):享受 Zod 链式 API 的便利和强大的类型推断带来的自动补全和静态检查,让 Bug 在编码阶段就无处遁形。
- 更高的代码健壮性:通过对请求参数和响应体进行严格的类型校验,极大地提升了应用的稳定性和可靠性。
对于任何使用 TypeScript 构建 Fastify 应用的开发者来说,拥抱 Zod 无疑是一项高回报的投资。
关于更完整的 schema 示例,可以参考我的一个开源项目,它完整地应用了本文提到的最佳实践: