首页
/ Fastify 入门指南:从第一个服务器到插件、数据校验与序列化的完整实战

Fastify 入门指南:从第一个服务器到插件、数据校验与序列化的完整实战

2026-09-05 14:43:38作者:邓越浪Henry

本文基于 Fastify 官方入门文档(docs/Guides/Getting-Started.md)并结合仓库源码编写,带你完成一次完整的上手流程:安装框架、搭建第一个 HTTP 服务器、理解 register 插件机制与异步启动(bootstrapping)、掌握基于 JSON Schema 的请求校验与响应序列化,并了解内容类型解析、测试注入与 fastify-cli 的使用方式。读完之后,你将能独立搭建一个多文件、可校验、可扩展的 Fastify 应用,并理解其关键 API 在 fastify.jslib/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.2fastify.jsVERSION = '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 式的错误处理:回调式通过 listenerr 参数捕获启动错误;Promise 式则用 try/catch 包裹 await fastify.listen(...)

监听地址:localhost、0.0.0.0 与 IPv6

官方文档特别提醒:示例默认只监听 localhost 的 127.0.0.1 接口。这一行为不是文档的口头约定,而是源码里的真实默认值。lib/server.jslisten 的签名是:

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.jsgetAddresses 会把 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 中,registerafterreadyonCloseclose 这些方法初始时都是 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

这个例子里引入了几个新概念,值得回顾:

  1. register 同时用于数据库连接器和路由。Fastify 按声明顺序串行加载插件:先加载 dbConnector,加载完成(数据库连接就绪)后才加载 routes,因此 routes 里能安全地使用 fastify.mongo
  2. 插件加载的触发时机:当你调用 fastify.listen()fastify.inject()fastify.ready() 时开始。这一点在源码中有清晰对应:fastify.js 中的 ready 函数负责执行 onReady hooks 并驱动 Avvio 启动流程;而 inject 在服务器尚未就绪时会先调用 this.ready(...)fastify.js),再执行注入请求——这就是为什么测试中可以用 inject 触发完整的插件加载。
  3. decorate API:MongoDB 插件通过 decoratemongo 对象挂到 Fastify 实例上供整个作用域使用。官方鼓励这种用法以提高代码复用、减少逻辑重复。装饰器的实现位于 lib/decorate.js,公开 API decoratedecorateReplydecorateRequesthasDecorator 等挂载在 fastify.js 的实例上。
  4. 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,包括 bodyquerystringparamsheaders

/**
 * @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.jsexamples/asyncawait.js 中使用的正是这套 response: { 200: ... } 写法,可以直接作为运行参考。更多细节见 Validation and Serialization 参考文档

六、解析请求负载

Fastify 原生解析 application/jsontext/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.jslib/server.js
路由声明 fastify.get/post/...fastify.route Routes 参考
监听与地址绑定 fastify.listen({ port, host }) lib/server.js
插件与异步启动 registerfastify-plugin Plugins 参考Plugins-Guide
请求校验 schema.body/params/querystring/headers Validation-and-Serializationlib/schema-controller.js
响应序列化 schema.response examples/simple.jslib/reply.js
请求体解析 request.body(JSON / 纯文本) ContentTypeParser 参考lib/content-type-parser.js
测试注入 fastify.inject Testing 指南
生态扩展 生态插件 Ecosystem 指南

掌握以上内容后,你可以沿着 Plugins-Guide 深入插件与作用域,沿 Validation and Serialization 构建严格的接口契约,并用 Testing 指南inject 方式为每个路由建立可靠的测试。这些章节与本文一脉相承,共同构成 Fastify 从入门到生产的完整路径。

登录后查看全文
热门项目推荐
相关项目推荐