Fastify — 让 schema 替你写校验和序列化的 Node.js 框架
是什么
Fastify 是一个 Node.js 的 web 框架——你给它路由和处理函数,它给你一个 HTTP 服务器。日常类比:像一个带模具的注塑机——Express 是手工捏陶,每个请求都要”看一眼形状再决定怎么处理”;Fastify 让你先做模具(schema),开机后所有请求被同一个模具一压成型,没有判断、没有反射。
最简单一段:
import Fastify from 'fastify'const app = Fastify()app.get('/hi', async () => ({ msg: 'hello' }))await app.listen({ port: 3000 })写起来跟 Express 几乎一样。差别藏在你看不见的地方:当你给路由配一个 JSON Schema,Fastify 在 listen() 之前就把 schema 编译成一段 JavaScript 函数。运行期不再”读 schema 判断 type”——直接调函数。这是它比 Express 快 3 倍的核心原因。
为什么重要
不理解 Fastify,下面这些事都没法解释:
- 为什么 Node.js 同样代码 Fastify 能跑 30k req/s、Express 只有 10k——差的 20k 哪里来
- 为什么 schema-first 框架(FastAPI / NestJS / Hono)这几年都流行——单一来源生成校验、序列化、文档
- 为什么 Fastify 的 plugin 不是
app.use()而是app.register()——封装边界设计的两条路 - 为什么 Matteo Collina(Node.js 核心维护者)愿意为这个框架站台
核心要点
Fastify 的设计可以拆成 三个支柱:
-
schema 先于代码:每个路由配 JSON Schema,启动期 Ajv 编译出 validator、fast-json-stringify 编译出 serializer。类比:开店前把所有菜单和容器提前印好,客人来了直接套用,不需要现场设计。
-
plugin encapsulation:每次
register()出一个子 instance。在子里加的装饰器、hook、路由都被关在子 scope 里。类比:每个插件像独立的房间,不会污染走廊。要全局生效得用fastify-plugin标注”穿墙”。 -
生命周期 hook 取代中间件链:八个固定阶段(onRequest → preParsing → preValidation → preHandler → handler → preSerialization → onSend → onResponse),顺序不可变。类比:流水线的固定工位,不像 Express 的”想插哪就插哪”。
底层还有 find-my-way 的 radix tree 路由(O(log n) 匹配)和 pino 异步 logger,三件套合起来叫”性能优先的现代 Node 框架”。
实践案例
案例 1:schema 同时管校验和序列化
app.post('/users', { schema: { body: { type: 'object', required: ['name', 'email'], properties: { name: { type: 'string', minLength: 1 }, email: { type: 'string', format: 'email' }, }, }, response: { 200: { type: 'object', properties: { id: { type: 'integer' }, name: { type: 'string' } } }, }, }, handler: async (req, reply) => { const { name } = req.body as { name: string } return { id: 1, name, secret: 'should-not-leak' } },})关键观察:客户端收不到 secret——response schema 没列它,序列化阶段被裁掉。这是安全特性也是性能优化(fast-json-stringify 跳过未列字段)。
案例 2:plugin encapsulation——子里的东西父看不见
app.decorate('rootHelper', () => 'global')
app.register(async (sub) => { sub.decorate('inSub', () => 'only here') sub.get('/sub', async () => sub.inSub()) // OK sub.get('/up', async () => sub.rootHelper()) // OK:子能看父})
// app.inSub // ❌ undefined:父看不到子逐步解释:register 内部 Object.create(app) 出 child;child 上加属性不会写回 parent。要”穿墙”得用 fastify-plugin 包:fp(myPlugin) 告诉 Fastify”这个插件不要起 scope”。
案例 3:hooks 替代 middleware
app.addHook('onRequest', async (req) => { req.log.info({ url: req.url }, 'incoming')})app.addHook('preHandler', async (req) => { if (!req.headers.authorization) throw new Error('Unauthorized')})对比 Express:Express 全是 app.use(mw),顺序靠注册顺序,语义全靠你自己记。Fastify 把”什么阶段做什么”写进 API:onRequest 永远第一、preHandler 永远在 handler 前。读代码时一眼知道执行顺序。
踩过的坑
-
不写 response schema = 性能归零:没 schema 的路由会回退到通用
JSON.stringify,速度跟 Express 一样。schema-first 的红利只对配了 schema 的路由生效。 -
schema 是契约不是建议:字段类型错配(schema 说 string、handler 返 number)会让 fast-json-stringify 在 prod 直接拼出
{"email":123}这种不合法但能 parse 的 JSON;handler 多返字段 schema 只列 3 个,会被悄悄裁掉——dev 模式 strict 能提前抓到 -
register 不是 use:第一次写的人会困惑”为什么我 decorate 的 helper 在外面调不到”。要全局共享得
fastify-plugin包一层。
适用 vs 不适用场景
适用:
- 高并发 REST API / JSON 微服务(schema 编译收益最大)
- 需要 OpenAPI 文档自动生成(schema 是单一来源)
- 中大型项目想用 plugin encapsulation 做边界隔离
不适用:
- 一次性脚本 / 简单内部工具(Express 几行更快)
- Edge runtime(Cloudflare Workers / Deno Deploy)——Fastify bundle 偏大,Hono / Elysia 更合适
- 想要类型自动从 schema 推到 TS——zod-based 的 Hono / Elysia 体验更顺,Fastify 要靠
@fastify/type-provider-typebox
历史小故事(可跳过)
- 2010 年:Express 1.0,定义了 Node.js 的 middleware 范式,但 schema 不是它的关注点
- 2013 年:Koa 出来,async 中间件优雅了,但仍是中间件链思维
- 2017 年:Matteo Collina(Node.js TSC 成员)和 Tomas Della Vedova 觉得”该有个 schema-first 的”,开了 Fastify v0.x
- 2018 年 8 月:v1.0 发布,性能立刻成为社区话题
- 2024 年:v5 要求 Node ≥ 20,weekly downloads ~3M,已经是 Node web 框架前三
学到什么
- “编译期做完,运行期不动” 是性能的黄金法则——schema → fn 这个套路适用于任何”反复用同一规则处理大量数据”的场景
- 封装边界 + 固定 Hook 比”想插就插” 更结构化——Fastify 用
Object.create在 JS 层做出 scope 隔离 + 八阶段固定 hook,思路可移植,且 FastAPI / Hono / Elysia 都已沿用 schema-first 路线,Express “代码即接口” 时代结束
延伸阅读
- 官方文档:fastify.dev(Getting Started 写得简洁,30 分钟跑通)
- Matteo Collina 的演讲:The Cost of Logging(讲为什么默认用 pino)
- Plugin 写法实战:fastify/example
- fastapi —— Python 同样是 schema-first 的代表,思路一脉相承
- playwright —— 同 Node 生态、同样把”编译/启动期把动态判断消除”做到极致
关联
- fastapi —— Python 版的 schema-first:Pydantic schema 同时管校验 / 文档 / 序列化
- warp —— Rust 里同代的”现代 web 框架”,但用类型而非 schema 表达 route
- playwright —— 同样 Node.js 生态,同样靠”启动期把动态消除”换性能
- hindley-milner —— schema 编译为 fn 的思路,与类型推导”把检查移到编译期”哲学相通
- ssa —— 编译期消除”运行期判断”的另一个工程典范
反向链接
- apollo-server —— Apollo Server — Node 端 GraphQL 服务端的事实标准
- appwrite —— Appwrite — 自己能装一遍的开源 Firebase
- bullmq —— BullMQ — Node.js 上的 Redis 任务队列
- centrifugo —— Centrifugo — Go 写的开源实时消息服务器
- connect-rpc —— ConnectRPC — 让 gRPC 在浏览器里裸跑的 RPC 协议
- deno —— Deno — 安全优先的 JS/TS 运行时
- discord-js —— discord.js — Node.js Discord API 客户端事实标准
- elysia —— Elysia — 长在 Bun 上的极致类型安全 Web 框架
- express —— Express — Node.js 最经典的 Web 框架
- fastapi —— FastAPI — 用 Python 类型注解写 API
- fiber —— Fiber — 把 Express 写法搬到 Go 上的高性能 web 框架
- got —— got — Node 端 HTTP 客户端的瑞士军刀
- grape —— Grape — 用 Ruby DSL 专写 REST API 的轻量框架
- graphql-yoga —— GraphQL Yoga — 跨运行时的轻量 GraphQL 服务器
- haraka —— Haraka — 用 Node.js 写插件链式架构的 SMTP 服务器
- hindley-milner —— Hindley-Milner — 编译器自己猜变量类型
- hono —— Hono — 多运行时 Web 框架
- ink —— ink — 用 React 组件树写终端 CLI
- jimp —— jimp — 哪都能跑的纯 JS 图像处理库
- koa —— Koa — async/await + ctx 对象 + 洋葱模型 的极简 Node.js web 框架
- ky —— ky — 把浏览器自带的 fetch 包成顺手工具
- lucia —— Lucia — 主动把自己降级为”学习资源”的 TS 认证库
- msw —— MSW — 让 mock 不改业务代码,在网络层透明拦截
- nestjs —— NestJS — 把 Angular 思想搬到 Node.js 后端的企业级框架
- next-js —— Next.js — React 全栈框架
- nodemailer —— Nodemailer — Node.js 发邮件的事实标准
- peerjs-server —— peerjs-server — 只管握手不管传话的 WebRTC 信令服务器
- pino —— pino — 日志不该阻塞热路径
- playwright —— Playwright — 跨浏览器自动化测试
- pocketbase —— PocketBase — 一个 Go 二进制就是完整的后端
- postgres-js —— postgres.js — 写 SQL 但语法层就防注入的 Node 客户端
- prom-client —— prom-client — Node 服务暴露监控指标的事实标准 SDK
- robyn —— Robyn — Rust 运行时的高性能 Python Web 框架
- sanic —— Sanic — 性能向 async Python 框架,对标 Node.js 高吞吐
- sharp —— sharp — 让 Node.js 处理图像快到不像 JS
- simple-peer —— simple-peer — 三行代码把两个浏览器直接连起来
- socket-io —— Socket.IO — 让浏览器和 Node.js 像打电话一样互相喊事件
- soketi —— Soketi — 自己跑一台 Pusher,把实时通信费砍到零头
- spin —— Spin — 用 WebAssembly 模块当 serverless handler 的开源框架
- ssa —— SSA — 静态单赋值形式
- steel-browser —— Steel Browser — 把 Chromium 包成 LLM agent 用的远端服务
- trpc —— tRPC — TS 端到端类型安全 RPC
- twirp —— Twirp — 用 protobuf 定义服务,但只走 HTTP/1.1 + JSON
- vertx —— Vert.x — Eclipse 出品的 polyglot reactive JVM toolkit,用事件总线 + verticle 把 Node.js 那套搬到多语言
- warp —— warp — Rust 里把请求处理拼成 Filter 积木的 web 框架
- zod —— Zod — TypeScript-first schema 验证