Fastify 入门指南:从第一个服务器到插件、数据校验与序列化的完整实战
本文基于 Fastify 官方入门文档(docs/Guides/Getting-Started.md)并结合仓库源码编写,带你完成一次完整的上手流程:安装框架、搭建第一个 HTTP 服务器、理解 register 插件机制与异步启动(bootstrapping)、掌握基于 JSON Schema 的请求校验与响应序列化,并了解内容类型解析、测试注入与 fastify-cli 的使用方式。读完之后,你将能独立搭建一个多文件、可校验、可扩展的 Fastify 应用,并理解其关键 API 在 fastify.js 与 lib/server.js 中的真实实现。
一、安装
Fastify 是一个基于 Node.js 的高性能、低开销 Web 框架(仓库描述为 "Fast and low overhead web framework, for Node.js",见 package.json)。安装方式与大多数 npm 包一致:
# npm
npm i fastify
# yarn
yarn add fastify
注意当前仓库的版本号:package.json 中标注为 6.0.0-alpha.2(fastify.js 中 VERSION = '6.0.0-alpha.2'),其核心依赖包括 avvio(插件加载器)、find-my-way(路由器)、pino(日志)、fast-json-stringify(响应序列化)与 light-my-request(请求注入测试)等,这些依赖决定了后文每个特性的底层实现归属。
二、第一个服务器
创建一个最简 Fastify 服务器需要三步:实例化、声明路由、监听端口。官方文档给出的完整示例(同时覆盖 ESM 与 CommonJS 两种写法)如下:
// 引入框架并实例化
// ESM
import Fastify from 'fastify'
const fastify = Fastify({
logger: true
})
// CommonJS
const fastify = require('fastify')({
logger: true
})
// 声明路由
fastify.get('/', function (request, reply) {
reply.send({ hello: 'world' })
})
// 启动服务器!
fastify.listen({ port: 3000 }, function (err, address) {
if (err) {
fastify.log.error(err)
process.exit(1)
}
// 服务器已监听在 ${address}
})
仓库内的 examples/simple.js 就是一个可运行的最简版本,它在官方示例基础上给 GET / 附加了 response 序列化 schema(后文第五节会讲到),而 examples/asyncawait.js 则演示了 async/await 风格。
async/await 风格
Fastify 原生支持 async/await,handler 可以直接 return 一个对象(或 Promise),框架会替你调用序列化与发送:
// ESM
import Fastify from 'fastify'
const fastify = Fastify({
logger: true
})
// CommonJS
const fastify = require('fastify')({
logger: true
})
fastify.get('/', async (request, reply) => {
return { hello: 'world' }
})
/**
* 启动服务器!
*/
const start = async () => {
try {
await fastify.listen({ port: 3000 })
} catch (err) {
fastify.log.error(err)
process.exit(1)
}
}
start()
如果项目使用 ECMAScript Modules(ESM),记得在 package.json 中加上 "type": "module"。两种风格的差异本质上是回调式与 Promise 式的错误处理:回调式通过 listen 的 err 参数捕获启动错误;Promise 式则用 try/catch 包裹 await fastify.listen(...)。
监听地址:localhost、0.0.0.0 与 IPv6
官方文档特别提醒:示例默认只监听 localhost 的 127.0.0.1 接口。这一行为不是文档的口头约定,而是源码里的真实默认值。lib/server.js 中 listen 的签名是:
function listen (listenOptions = { port: 0, host: 'localhost' }, cb = undefined)
并且当 host 未显式提供时会回退到 'localhost'(lib/server.js)。因此:
- 想监听所有 IPv4 接口,应显式指定
0.0.0.0:
fastify.listen({ port: 3000, host: '0.0.0.0' }, function (err, address) {
if (err) {
fastify.log.error(err)
process.exit(1)
}
fastify.log.info(`server listening on ${address}`)
})
- 指定
::1表示仅接受本机 IPv6 连接;指定::则接受所有 IPv6 地址的连接,并在操作系统支持时同时接受 IPv4 连接。 - 部署到 Docker(或同类容器)时,用
0.0.0.0或::暴露应用是最简单的方式。 - 使用
0.0.0.0时,回调中拿到的address是通配符所指代的第一个地址。这一点在源码中可以看到对应实现:lib/server.js 的getAddresses会把0.0.0.0展开为本机所有 IPv4 网卡地址(内网接口排在前面),并逐条记录Server listening at <address>日志,最终返回的第一个地址即为回调参数。
另外,源码注释中说明了对 localhost 的额外处理:参考 Node.js 的 dns.lookup 结果同时绑定 127.0.0.1 与 ::1(见 multipleBindings 函数,lib/server.js),这与文档"默认监听 localhost"的描述是一致的。
三、第一个插件:register 与异步启动
"在 JavaScript 中万物皆对象,在 Fastify 中万物皆插件"。这是理解 Fastify 架构的钥匙。先看一个把路由移到外部文件的例子(路由声明的完整规则可参考 Routes 参考文档):
入口文件:
// ESM
import Fastify from 'fastify'
import routes from './our-first-routes.js'
/**
* @type {import('fastify').FastifyInstance}
*/
const fastify = Fastify({
logger: true
})
fastify.register(routes)
fastify.listen({ port: 3000 }, function (err, address) {
if (err) {
fastify.log.error(err)
process.exit(1)
}
})
// CommonJS
const fastify = require('fastify')({
logger: true
})
fastify.register(require('./our-first-routes'))
fastify.listen({ port: 3000 }, function (err, address) {
if (err) {
fastify.log.error(err)
process.exit(1)
}
})
路由文件 our-first-routes.js:
/**
* 封装路由的插件
* @param {FastifyInstance} fastify 被封装的 Fastify 实例
* @param {Object} options 插件选项
*/
async function routes (fastify, options) {
fastify.get('/', async (request, reply) => {
return { hello: 'world' }
})
}
// ESM
export default routes
// CommonJS
module.exports = routes
这个例子里用的是 register API——它是 Fastify 框架的核心,也是添加路由、插件的唯一途径。这一点在源码中可以直接印证:fastify.js 中,register、after、ready、onClose、close 这些方法初始时都是 null,随后由 Avvio 插件加载器注入:
const avvio = Avvio(fastify, {
autostart: false,
timeout: isNaN(avvioPluginTimeout) === false ? avvioPluginTimeout : defaultInitOptions.pluginTimeout,
expose: {
use: 'register'
}
})
// 覆写 override 以支持插件的封装(encapsulation)
avvio.override = override
avvio.override = override(实现位于 lib/plugin-override.js)正是 Fastify 作用域封装的入口:每次 register 都会基于 override 产生一个子实例,子实例的路由、装饰器、hooks 被限制在自己的作用域内——这也是官方推荐"先注册生态插件、再注册自定义插件"顺序的原因。
数据库连接的异步启动
真实应用中,很多功能(比如数据库连接)必须在服务器开始接受连接之前就绪。典型做法是用复杂回调或手工 Promise 把框架 API 和业务代码搅在一起;而 Fastify 依靠 register 的串行加载机制把它内化了:插件按声明顺序逐个加载,且当前插件加载完成后才加载下一个。
以 @fastify/mongodb 为例。先安装依赖:
npm i fastify-plugin @fastify/mongodb
server.js:
// ESM
import Fastify from 'fastify'
import dbConnector from './our-db-connector.js'
import routes from './our-first-routes.js'
const fastify = Fastify({
logger: true
})
fastify.register(dbConnector)
fastify.register(routes)
fastify.listen({ port: 3000 }, function (err, address) {
if (err) {
fastify.log.error(err)
process.exit(1)
}
})
CommonJS 版本把 import 换成 require 即可(fastify.register(require('./our-db-connector')) 等)。
our-db-connector.js:
// ESM
import fastifyPlugin from 'fastify-plugin'
import fastifyMongo from '@fastify/mongodb'
/**
* @param {FastifyInstance} fastify
* @param {Object} options
*/
async function dbConnector (fastify, options) {
fastify.register(fastifyMongo, {
url: 'mongodb://localhost:27017/test_database'
})
}
// 用 fastify-plugin 包装插件函数,
// 可把插件内部声明的装饰器(decorators)和 hooks 暴露给父作用域
export default fastifyPlugin(dbConnector)
// CommonJS
const fastifyPlugin = require('fastify-plugin')
async function dbConnector (fastify, options) {
fastify.register(require('@fastify/mongodb'), {
url: 'mongodb://localhost:27017/test_database'
})
}
module.exports = fastifyPlugin(dbConnector)
our-first-routes.js(路由通过 fastify.mongo 装饰器访问数据库):
/**
* 提供封装路由的插件
* @param {FastifyInstance} fastify 被封装的 fastify 实例
* @param {Object} options 插件选项
*/
async function routes (fastify, options) {
const collection = fastify.mongo.db.collection('test_collection')
fastify.get('/', async (request, reply) => {
return { hello: 'world' }
})
fastify.get('/animals', async (request, reply) => {
const result = await collection.find().toArray()
if (result.length === 0) {
throw new Error('No documents found')
}
return result
})
fastify.get('/animals/:animal', async (request, reply) => {
const result = await collection.findOne({ animal: request.params.animal })
if (!result) {
throw new Error('Invalid value')
}
return result
})
const animalBodyJsonSchema = {
type: 'object',
required: ['animal'],
properties: {
animal: { type: 'string' }
}
}
const schema = {
body: animalBodyJsonSchema
}
fastify.post('/animals', { schema }, async (request, reply) => {
// 用 `request.body` 获取客户端发送的数据
const result = await collection.insertOne({ animal: request.body.animal })
return result
})
}
module.exports = routes
这个例子里引入了几个新概念,值得回顾:
register同时用于数据库连接器和路由。Fastify 按声明顺序串行加载插件:先加载dbConnector,加载完成(数据库连接就绪)后才加载routes,因此routes里能安全地使用fastify.mongo。- 插件加载的触发时机:当你调用
fastify.listen()、fastify.inject()或fastify.ready()时开始。这一点在源码中有清晰对应:fastify.js 中的ready函数负责执行onReadyhooks 并驱动 Avvio 启动流程;而inject在服务器尚未就绪时会先调用this.ready(...)(fastify.js),再执行注入请求——这就是为什么测试中可以用inject触发完整的插件加载。 decorateAPI:MongoDB 插件通过decorate把mongo对象挂到 Fastify 实例上供整个作用域使用。官方鼓励这种用法以提高代码复用、减少逻辑重复。装饰器的实现位于 lib/decorate.js,公开 APIdecorate、decorateReply、decorateRequest、hasDecorator等挂载在 fastify.js 的实例上。fastify-plugin包装器:插件默认被封装在自己的作用域内,其内部声明的装饰器对外不可见;用fastifyPlugin(dbConnector)包装后,装饰器和 hooks 会被提升到父作用域(作用域细节可参考 Plugins 参考文档的 Handle the scope 一节)。
想更深入地了解插件机制、如何开发新插件以及完整的异步启动方案,请阅读 插件指南(Plugins-Guide)。
推荐的插件加载顺序
为保证应用行为一致、可预测,官方强烈推荐按以下顺序加载代码:
└── plugins(来自 Fastify 生态的插件)
└── your plugins(你的自定义插件)
└── decorators(装饰器)
└── hooks(钩子)
└── your services(你的业务服务)
这样你在当前作用域内始终可以访问已声明的全部属性。
同时,Fastify 提供了坚固的封装模型,帮助你把应用构建成相互独立的服务。如果只想给一部分路由注册某个插件,只需在对应的服务内部复制上述结构即可:
└── plugins(来自 Fastify 生态的插件)
└── your plugins(你的自定义插件)
└── decorators(装饰器)
└── hooks(钩子)
└── your services(你的业务服务)
│
└── service A
│ └── plugins(来自 Fastify 生态的插件)
│ └── your plugins(你的自定义插件)
│ └── decorators(装饰器)
│ └── hooks(钩子)
│ └── your services(你的业务服务)
│
└── service B
└── plugins(来自 Fastify 生态的插件)
└── your plugins(你的自定义插件)
└── decorators(装饰器)
└── hooks(钩子)
└── your services(你的业务服务)
四、校验你的数据
数据校验是框架的核心概念。Fastify 使用 JSON Schema 校验入站请求。给路由传一个 options 对象,其中 schema 键包含该路由的全部 schema,包括 body、querystring、params 和 headers:
/**
* @type {import('fastify').RouteShorthandOptions}
* @const
*/
const opts = {
schema: {
body: {
type: 'object',
properties: {
someKey: { type: 'string' },
someOtherKey: { type: 'number' }
}
}
}
}
fastify.post('/', opts, async (request, reply) => {
return { hello: 'world' }
})
从源码结构看,schema 的处理统一收口在 lib/schema-controller.js:实例化时通过 SchemaController.buildSchemaController(null, options.schemaController) 构建(fastify.js),校验器编译器默认由 @fastify/ajv-compiler 提供(见 package.json 依赖),也允许用 setValidatorCompiler 替换为自定义实现。完整的校验与序列化规则参见 Validation and Serialization 参考文档。
五、序列化你的数据
Fastify 对 JSON 有一等公民级的支持,JSON 请求体解析与 JSON 响应输出都做了深度优化。要加速 JSON 序列化,使用 schema 选项的 response 键:
/**
* @type {import('fastify').RouteShorthandOptions}
* @const
*/
const opts = {
schema: {
response: {
200: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
}
fastify.get('/', opts, async (request, reply) => {
return { hello: 'world' }
})
指定这样的 schema 后,序列化速度可以提升到 2~3 倍。更重要的是,它还起到防泄漏作用:Fastify 只会序列化响应 schema 中声明的数据,schema 之外的敏感字段不会意外下发。底层实现上,响应 schema 会编译成 fast-json-stringify 的专用序列化函数(依赖见 package.json),而 examples/simple.js 与 examples/asyncawait.js 中使用的正是这套 response: { 200: ... } 写法,可以直接作为运行参考。更多细节见 Validation and Serialization 参考文档。
六、解析请求负载
Fastify 原生解析 application/json 与 text/plain 两种请求负载,解析结果挂在 Request 对象的 request.body 上。下面的例子把解析后的 body 原样返回给客户端:
/**
* @type {import('fastify').RouteShorthandOptions}
*/
const opts = {}
fastify.post('/', opts, async (request, reply) => {
return request.body
})
源码印证:lib/content-type-parser.js 在初始化 ContentTypeParser 时默认注册了两个解析器,并把它们列入解析器白名单:
this.customParsers.set('application/json', new Parser(true, false, bodyLimit, this[kDefaultJsonParse]))
this.customParsers.set('text/plain', new Parser(true, false, bodyLimit, defaultPlainTextParser))
this.parserList = ['application/json', 'text/plain']
JSON 解析基于 secure-json-parse(防原型污染,onProtoPoisoning/onConstructorPoisoning 选项可在实例化时定制,见 fastify.js)。要支持其他 Content-Type,请阅读 Content-Type Parser 参考文档,它同时介绍了默认解析能力和自定义解析器的注册方式。
七、扩展你的服务器与测试
Fastify 的设计哲学是"极简内核 + 生态扩展":它有意不做 "batteries included" 的框架,而是依赖一个庞大的 生态系统 来补齐文件上传、认证、静态资源等功能。
在测试方面,Fastify 不内置测试框架,但官方推荐使用基于框架自身特性的测试方式——核心就是 fastify.inject():它基于 light-my-request 在进程内模拟完整的 HTTP 请求/响应链路,无需真正监听端口。从源码看,inject 会按需加载 light-my-request 并调用路由处理器 httpHandler,且在服务器未就绪时自动等待插件加载完成(fastify.js)。完整的测试最佳实践见 Testing 指南。
八、用 CLI 运行服务器
Fastify 生态中的 fastify-cli 提供了脚手架与项目管理能力(它是一个独立包,需单独安装):
npm i fastify-cli
也可以用 -g 全局安装。然后在 package.json 中加入:
{
"scripts": {
"start": "fastify start server.js"
}
}
并创建你的服务器文件。注意 fastify-cli 的约定:服务器文件导出的是一个插件函数(与 examples/plugin.js 展示的经典插件形式一致),由 CLI 负责实例化和启动:
// server.js
'use strict'
module.exports = async function (fastify, opts) {
fastify.get('/', async (request, reply) => {
return { hello: 'world' }
})
}
最后执行:
npm start
九、小结:入门后的学习路径
回顾本文覆盖的 Fastify 入门主线:
| 能力 | 核心 API | 深入文档 / 源码 |
|---|---|---|
| 安装与实例化 | Fastify({ logger: true }) |
fastify.js、lib/server.js |
| 路由声明 | fastify.get/post/...、fastify.route |
Routes 参考 |
| 监听与地址绑定 | fastify.listen({ port, host }) |
lib/server.js |
| 插件与异步启动 | register、fastify-plugin |
Plugins 参考、Plugins-Guide |
| 请求校验 | schema.body/params/querystring/headers |
Validation-and-Serialization、lib/schema-controller.js |
| 响应序列化 | schema.response |
examples/simple.js、lib/reply.js |
| 请求体解析 | request.body(JSON / 纯文本) |
ContentTypeParser 参考、lib/content-type-parser.js |
| 测试注入 | fastify.inject |
Testing 指南 |
| 生态扩展 | 生态插件 | Ecosystem 指南 |
掌握以上内容后,你可以沿着 Plugins-Guide 深入插件与作用域,沿 Validation and Serialization 构建严格的接口契约,并用 Testing 指南 的 inject 方式为每个路由建立可靠的测试。这些章节与本文一脉相承,共同构成 Fastify 从入门到生产的完整路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00