ARTICLE · 1149694
插件、Schema 和高性能 JSON,不只是 Express 平替



第一次用 Fastify,最直观的感受是:它不是只给你一个 req、一个 res,然后祝你好运。
它把很多后端工程里绕不开的事情放到了台面上:
插件怎么隔离;
生命周期钩子怎么排队;
JSON 输入怎么校验;
JSON 输出怎么更快序列化;
日志怎么默认带上上下文;
路由怎么高效匹配。
示例很简单:
importFastifyfrom'fastify'constfastify=Fastify({ logger: true })fastify.get('/', async (request, reply) => {return { hello: 'fastify' }})awaitfastify.listen({ port: 3000 })
但如果你只把它当“另一个 Express”,就会错过它最有意思的部分。

源码地图:先认识这些文件
读 Fastify 可以先从这些文件下手:
fastify.js:应用初始化和主装配逻辑;lib/handle-request.js:请求处理主路径;lib/route.js:路由注册相关逻辑;lib/request.js:请求对象封装;lib/reply.js:响应对象封装;lib/hooks.js:钩子定义和调度;lib/plugin-override.js、lib/plugin-utils.js:插件封装相关;lib/schema-controller.js、lib/validation.js:Schema 校验与序列化;lib/error-handler.js:错误处理。
依赖也能说明设计方向:
find-my-way:高性能 HTTP 路由;avvio:插件启动和封装;pino:日志;fast-json-stringify:基于 Schema 的 JSON 字符串生成;@fastify/ajv-compiler:JSON Schema 校验;secure-json-parse:更安全地解析 JSON;light-my-request:注入式请求测试。
个人观点:只看文件列表,就能看出 Fastify 的性格。它不是“先极简,剩下你们自己拼”,而是把后端常见能力选好、接好,同时尽量保持插件化。

路由:不是遍历数组,而是交给路由树
Express 风格里,很多人容易把路由理解成一个线性数组:请求来了,从前往后试。
Fastify 使用 find-my-way,更接近一棵按路径结构组织的路由树。比如这些路由:
GET /usersGET /users/:idPOST /usersGET /orders/:id
树会把公共前缀和动态节点组织起来,匹配时少走冤枉路。
注册路由时,你会看到类似这样的 API:
fastify.route({method: 'GET',url: '/users/:id',schema: {params: {type: 'object',required: ['id'],properties: {id: { type: 'string' } } } },handler: asyncfunction (request, reply) {return { id: request.params.id } }})
更常见的简写是:
fastify.get('/users/:id', asyncrequest=> {return { id: request.params.id }})
源码里路由注册不只是保存函数,还会处理:
方法和路径;
参数、query、body 的 Schema;
响应 Schema;
路由级钩子;
错误处理和序列化器;
封装上下文。
这也是为什么 Fastify 的路由配置看起来比单纯“路径 + 函数”厚一些。它把不少运行时信息提前准备好了。

插件系统:register 不是简单地执行函数
Fastify 的插件系统是理解它的关键。看一个插件:
asyncfunctionauthPlugin(app, options) {app.decorate('checkToken', asyncfunction (request) {request.user=awaitverifyToken(request.headers.authorization) })app.addHook('preHandler', asyncrequest=> {awaitapp.checkToken(request) })}fastify.register(authPlugin)
如果只用 Express 思维,你可能觉得 register 就是调用函数。但 Fastify 借 avvio 管理插件启动,并提供封装语义。插件内部添加的装饰器、钩子,默认属于这个插件上下文;父子关系、注册顺序、异步启动都有规则。
一个常见例子:
fastify.register(asyncfunctionpluginA(app) {app.decorate('foo', 'foo')app.get('/a', asyncfunction () {return { foo: this.foo } })})fastify.get('/b', asyncfunction () {return { foo: this.foo }})
在 /b 里未必能拿到插件内部装饰的 foo。这不是 bug,而是封装边界。想共享到父级或根实例,需要正确使用 fastify-plugin 之类方式,把封装“打开”。
个人观点:这个设计一开始可能让人觉得别扭,但大型项目里它能避免所有插件都往全局对象上乱扔东西。全局一时爽,排查火葬场,这种剧情大家都熟。

Schema:输入校验和输出序列化是两件事
Fastify 最有辨识度的能力之一是 JSON Schema。
比如:
constschema= {body: {type: 'object',required: ['name', 'age'],properties: {name: { type: 'string', minLength: 1 },age: { type: 'integer', minimum: 18 } } }}fastify.post('/users', { schema }, asyncrequest=> {returnrequest.body})
请求体不合法时,在进入业务 handler 前就会被拦下。业务代码里不用反复写:
if (!body.name) ...if (typeofbody.age!=='number') ...
输出也可以定义 Schema:
fastify.post('/users', { schema: { response: { 200: { type: 'object', properties: { id: { type: 'integer' }, name: { type: 'string' } } } } }}, async request => { return { id: 1, name: request.body.name, password: 'should-not-leak' }})
响应 Schema 配合 fast-json-stringify,既可能提升序列化速度,也能帮助约束输出。上面的 password 如果不在响应 Schema 里,就不应该指望它被正常返回。
这背后的思路可以粗略写成:
const validate = compileSchema(schema.body)const stringify = buildSerializer(schema.response[200])async function handler(request, reply) { if (!validate(request.body)) { throw validationError(validate.errors) } const data = await business(request.body) reply.header('content-type', 'application/json') reply.send(stringify(data))}
真实代码要处理多状态码、错误格式、引用 Schema、共享 schema storage,远比这复杂。但主干很清楚:提前编译,运行时复用。

生命周期:钩子不是摆设,而是分层的好机会
Fastify 有多种生命周期钩子,比如:
onRequest:请求刚进来,适合鉴权、请求 id;preParsing:解析 body 前;preValidation:校验前;preHandler:业务处理前;onSend:发送前;onResponse:响应完成后;onError:出错时。
示例:
fastify.addHook('onRequest', async (request, reply) => { request.startTime = performance.now()})fastify.addHook('onResponse', async (request, reply) => { const cost = performance.now() - request.startTime request.log.info({ cost }, 'request completed')})
个人观点:钩子多不代表每个都要用。最舒服的方式是按职责放:
鉴权不要塞进业务 handler;
日志不要散落在每个路由;
响应统一改写放
onSend;参数和 body 不合法交给 Schema;
业务异常和系统异常分层处理。


实操踩坑:从 Express 迁过来最容易懵的地方
1. 箭头函数会丢掉 this
普通函数写法:
fastify.get('/ping', async function (request, reply) { request.log.info('ping') return this.someDecorator})
如果改成箭头函数:
fastify.get('/ping', async (request, reply) => { return this.someDecorator // 这里的 this 不是 Fastify 实例})
需要访问封装实例时,用普通函数;不需要 this 时,箭头函数没问题。
2. 插件注册顺序不是装饰品
Fastify 的插件、路由和钩子都受注册顺序影响。你不能先注册业务路由,再期待它自动吃到后来注册的鉴权钩子:
fastify.register(routes)fastify.register(authPlugin) // 未必能按你想象影响前面的 routes
更稳的方式是先注册基础能力,再注册业务模块。
3. 响应 Schema 会“帮你少返回字段”
如果接口响应里突然少字段,先看 response schema,而不是立刻怀疑 ORM。很多时候是序列化器按合同办事,没列进去的字段就别出门。
4. return 和 reply.send 二选一为主
异步 handler 里可以直接返回数据:
fastify.get('/', async () => ({ ok: true }))也可以:
fastify.get('/', async (request, reply) => { return reply.code(201).send({ ok: true })})
但不要混用出复杂副作用,例如先 reply.send(),后面又返回另一个对象。框架会努力处理,但代码可读性会先报警。

选型对比:Fastify、Express、Koa
Fastify 更适合
中大型 Node API 服务;
重视 JSON Schema、日志、插件边界;
需要较好的性能和可观测性;
团队能接受更明确的工程约定。
Express 更适合
老项目维护;
依赖大量传统 Express 中间件;
团队熟悉度优先;
快速实现简单服务。
Koa 更适合
想要轻量核心;
喜欢洋葱模型;
愿意自己挑选和组装组件。
个人观点:Fastify 不是“性能数字更高所以必选”。如果团队没有使用 Schema 的习惯,插件边界也不理解,直接上 Fastify 可能只是换一种方式写混乱。框架优势要配合工程习惯才能兑现。

二次开发思路:从公司级基础插件开始
可以把 Fastify 当作内部服务底座,做这些封装。
统一错误码插件
async function errorCodePlugin(app) { app.setErrorHandler((error, request, reply) => { if (error.validation) { return reply.status(400).send({ code: 'INVALID_REQUEST', message: error.message }) } request.log.error({ err: error }) reply.status(500).send({ code: 'INTERNAL_ERROR', message: 'Internal Server Error' }) })}
租户识别插件
async function tenantPlugin(app) { app.addHook('preHandler', async request => { const tenantId = request.headers['x-tenant-id'] if (!tenantId) { throw app.httpErrors.unauthorized('missing tenant') } request.tenantId = String(tenantId) })}
内部服务注册约定
可以按模块拆分:
plugins/ error.js auth.js tenant.js logger.jsroutes/ users.js orders.jsserver.js
二次开发还可以做:
根据路由 Schema 自动生成 OpenAPI;
统一给内部接口加权限标签;
基于
light-my-request做模块级集成测试;封装数据库事务上下文;
给日志自动注入用户、租户、requestId。

社区背景:规则文件多,说明项目不只靠热情发电
仓库里能看到 CONTRIBUTING.md、CODE_OF_CONDUCT.md、GOVERNANCE.md、PROJECT_CHARTER.md、SECURITY.md、SPONSORS.md 等文件。它们说明 Fastify 不只是写代码,还在维护协作方式、治理结构和安全流程。
个人观点:成熟开源项目不只是“代码能跑”。谁能决策、安全问题怎么报、插件如何协作、版本如何发布,这些内容在项目变大后都很关键。Fastify 在这方面的边界感,比单纯 benchmark 更值得关注。

它的重点不是快,而是先把规则立住
读 Fastify,可以抓住三条主线:
路由用
find-my-way做高效匹配;插件靠
avvio管理启动和封装;Schema 同时参与输入校验和输出序列化。
它不是 Express 的简单换皮,也不是只拿性能当招牌的网红框架。更像一个把后端工程常见规矩提前摆上桌的 Node 框架。
当然,规矩多了一开始会有点束缚。但等你维护几十个服务、上百个路由、若干团队共同写插件时就会发现:少一点“我先挂全局再说”,世界真的会清净很多。


获取更多开源新资讯