log
码中赤兔

Fastify 最佳实践 #4: 用 Zod 替代 Ajv 管理 Schema

发布于 2025年6月23日
更新于 2025年6月26日
12 分钟阅读
TypeScript

在构建服务端应用时,对传入的 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;

这种方式虽然能完成任务,但缺点也显而易见:

  1. 定义繁琐冗长Ajv 的 JSON Schema 语法相对啰嗦。当校验规则复杂、字段繁多时,Schema 对象会变得异常庞大,难以阅读和维护。
  2. 类型系统割裂Ajv 的 Schema 定义和 TypeScript 的类型系统是完全独立的。这意味着你为了运行时校验定义了一遍 Schema,却还需要再手动声明一次对应的 TypeScript interfacetype 来获得编译时类型提示,这无疑增加了重复工作,也带来了两者不一致的风险。虽然可以借助 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 示例,可以参考我的一个开源项目,它完整地应用了本文提到的最佳实践:

fastify-best-practice on GitHub

关于

分享技术见解、经验和思考的个人博客

联系方式

  • Email: hushukang_blog@proton.me
  • GitHub

© 2025 码中赤兔. 版权所有