Fastify 高性能框架:Schema 验证与插件化
Fastify 高性能框架:Schema 验证与插件化
Fastify 是专为高性能而生的 Node.js Web 框架,它在保证极高吞吐量的同时,通过内置的 Schema 验证和强大的插件系统提供了严谨的开发体验。本教程将带你从零开始,理解 Fastify 的核心思想,并掌握 Schema 验证与插件化的实战用法。
为什么选择 Fastify?
Fastify 的设计目标非常明确:在提供优秀开发者体验的前提下,成为最快的 Web 框架之一。它的核心优势包括:
- 极致的性能:在各类基准测试中表现卓越,得益于其高度优化的路由、序列化、验证机制。
- 开箱即用的 Schema 验证:使用 JSON Schema 对请求和响应进行快速验证与序列化,既安全又高效。
- 强大的插件体系:通过插件机制封装逻辑、扩展功能,代码结构清晰,复用性强。
- 原生支持 TypeScript:虽然核心用 JavaScript 编写,但提供了完善的类型定义。
环境准备
确保你的开发环境已安装 Node.js 18+ 版本。然后新建项目并安装 Fastify:
mkdir fastify-tutorial
cd fastify-tutorial
npm init -y
npm install fastify
创建第一个 Fastify 服务器
Fastify 的最小服务器只需几行代码。创建一个 app.js 文件:
// app.js
const fastify = require('fastify')({ logger: true });
// 声明一个路由
fastify.get('/', async (request, reply) => {
return { hello: 'world' };
});
// 启动服务器
const start = async () => {
try {
await fastify.listen({ port: 3000 });
console.log('服务器正在监听 http://localhost:3000');
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
运行 node app.js,访问 http://localhost:3000,你将看到 {"hello":"world"}。
日志与开发体验
在构建实例时传入 { logger: true } 会启用 Pino 日志,方便调试。生产环境中你可以配置不同的日志级别。
Schema 验证:让请求更安全
没有验证的接口容易产生不可预期的数据,而 Fastify 将 Schema 验证深度整合到了路由中。你只需定义 JSON Schema,框架会自动完成校验和序列化,极大提升性能。
基本用法:验证请求体(Body)
假设我们要接收一个创建用户的数据,要求 name 为字符串且必填,age 为整数。
fastify.post('/users', {
schema: {
body: {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string' },
age: { type: 'integer' }
}
}
},
handler: async (request, reply) => {
// request.body 已经被验证并保证了类型
const { name, age } = request.body;
// 这里可以放心写入数据库等操作
return { result: 'ok', user: { name, age } };
}
});
如果客户端发送了不合规的 JSON,例如 age 为 "28",Fastify 会返回 400 错误,并附带清晰的错误信息:
{
"statusCode": 400,
"error": "Bad Request",
"message": "body/age must be integer"
}
验证多种请求数据
Fastify 支持对 body、query、params、headers 分别定义 Schema。
fastify.get('/users/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'integer' }
}
},
querystring: {
type: 'object',
properties: {
detail: { type: 'boolean' }
}
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' }
}
}
}
},
handler: async (request, reply) => {
const { id } = request.params; // id 保证是整数
const { detail } = request.query; // detail 保证是布尔值或 undefined
// 从数据库获取用户...
const user = { id, name: 'Alice' };
return user;
}
});
响应 Schema 特别重要:它不仅能验证返回数据,还会触发序列化优化。Fastify 会根据定义的 response Schema 直接用 fast-json-stringify 生成的高效函数序列化数据,比原生 JSON.stringify 快得多。
自定义错误处理
你可以在全局或路由级别自定义验证失败的处理逻辑。
fastify.setErrorHandler((error, request, reply) => {
if (error.validation) {
// error.validation 包含验证失败的详细信息
reply.status(400).send({
ok: false,
message: '输入参数错误',
details: error.validation
});
} else {
reply.send(error);
}
});
Schema 的复用与聚合
你可以将常用的 Schema 定义提取出来,通过 addSchema 添加共享 Schema,并使用 JSON Schema 的 $ref 引用。
fastify.addSchema({
$id: 'user',
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' }
}
});
fastify.post('/users', {
schema: {
body: {
$ref: 'user'
},
response: {
201: {
$ref: 'user'
}
}
},
handler: async (request, reply) => {
// ...
reply.code(201);
return request.body;
}
});
插件化架构:构建可维护的应用
插件是 Fastify 构建大型应用的基石。所有功能(路由、工具函数、数据库连接等)都可以封装成插件,并通过 register 进行注册。这带来了:
- 逻辑封装:每个插件拥有独立的作用域,子实例可以继承或覆盖父实例的装饰、钩子。
- 顺序保证:插件按注册顺序加载,如同洋葱模型一样包裹请求处理流程。
- 易于测试:每个插件都可以独立测试。
编写一个简单的插件
插件是一个接受 fastify 实例和可选的配置对象作为参数的函数。
// plugins/db-connector.js
const fp = require('fastify-plugin');
async function dbConnector(fastify, opts) {
// 模拟数据库连接
const db = {
users: {
find: async (id) => ({ id, name: `User ${id}` })
}
};
// 将数据库实例装饰到 fastify 上
fastify.decorate('db', db);
// 关闭钩子:应用退出时断开连接
fastify.addHook('onClose', async (instance, done) => {
// 模拟断开
console.log('数据库连接已关闭');
});
}
// 使用 fastify-plugin 包装,使其成为可直接注册的插件
module.exports = fp(dbConnector, {
name: 'db-connector' // 插件名称,可选
});
注意 fastify-plugin 的用法:它不是必需的,但强烈推荐。使用 fp() 包装后,插件就不会创建一个新的作用域,而是直接向父实例添加装饰,避免了重复包装的作用域开销,尤其适用于数据库、工具类等需要暴露给全应用的插件。
注册并使用插件
// app.js
const fastify = require('fastify')({ logger: true });
const dbConnector = require('./plugins/db-connector');
fastify.register(dbConnector, {
url: 'mongodb://localhost:27017/mydb' // 配置参数
});
fastify.get('/user/:id', async (request, reply) => {
const { id } = request.params;
// 使用插件装饰的 db 对象
const user = await fastify.db.users.find(parseInt(id));
return user;
});
fastify.listen({ port: 3000 }).then(() => {
console.log('服务器已启动');
});
插件作用域与钩子
Fastify 的注册系统会创建父子实例关系。子插件可以添加自己的路由、钩子,甚至覆盖父实例的设置(如日志级别),但这些修改不会影响父实例。
通过 addHook,你可以在请求生命周期的各个阶段插入逻辑,例如:
onRequest:请求到达时,路由之前preHandler:路由匹配后,实际处理函数之前onSend:响应发送前onResponse:响应已发送
fastify.addHook('onRequest', async (request, reply) => {
// 身份验证检查示例
if (!request.headers.authorization) {
reply.code(401).send({ error: '未授权' });
}
});
综合示例:用户管理 API
下面将 Schema 验证与插件结合,构建一个小型的用户 API。
目录结构:
project/
├─ app.js
├─ plugins/
│ └─ db.js
└─ routes/
└─ users.js
plugins/db.js – 模拟数据库插件:
const fp = require('fastify-plugin');
module.exports = fp(async (fastify, opts) => {
const store = new Map();
fastify.decorate('db', {
create: (user) => { store.set(user.id, user); return user; },
get: (id) => store.get(id),
list: () => [...store.values()]
});
});
routes/users.js – 用户路由插件,包含 Schema 定义:
const fp = require('fastify-plugin');
// 共享的用户 Schema
const userSchema = {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
email: { type: 'string', format: 'email' }
}
};
module.exports = fp(async (fastify, opts) => {
fastify.addSchema({
$id: 'user',
...userSchema
});
// 创建用户
fastify.post('/', {
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string' },
email: { type: 'string', format: 'email' }
}
},
response: {
201: { $ref: 'user' }
}
},
handler: async (request, reply) => {
const id = Date.now(); // 模拟 ID 生成
const user = { id, ...request.body };
fastify.db.create(user);
reply.code(201);
return user;
}
});
// 获取用户列表
fastify.get('/', {
schema: {
response: {
200: {
type: 'array',
items: { $ref: 'user' }
}
}
},
handler: async () => fastify.db.list()
});
});
app.js – 组装应用:
const fastify = require('fastify')({ logger: true });
// 注册数据库插件
fastify.register(require('./plugins/db'));
// 注册用户路由插件,并设置前缀
fastify.register(require('./routes/users'), { prefix: '/users' });
fastify.listen({ port: 3000 }).then((address) => {
fastify.log.info(`服务器运行于 ${address}`);
});
现在你可以用 cURL 或 Postman 测试:
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}'
# 返回: {"id":169384..., "name":"Alice","email":"alice@example.com"}
# 错误格式演示
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Bob","email":"not-an-email"}'
# 返回 400 错误及验证消息
curl http://localhost:3000/users
# 返回用户数组
进阶技巧
-
序列化加速:尽量为路由定义
responseSchema,Fastify 内部会缓存编译后的序列化函数,获得极大性能提升。 -
封装验证规则:对常用的字符串格式(如 email、uri)可以定义自己的格式验证函数:
fastify.addSchemaFormat('phone', (value) => /^1[3-9]\d{9}$/.test(value)); -
插件顺序与依赖:如果你的插件依赖另一个插件,可以在
register时传入dependencies选项,或者利用异步注册的顺序保证。 -
使用 TypeScript:Fastify 提供了完善的泛型支持,你可以通过
FastifyRequest<{ Params: ..., Body: ... }>获得精确的类型推导,与 Schema 配合简直是天作之合。
总结
Fastify 通过 Schema 优先 的验证方式和 模块化插件体系,完美平衡了性能与健壮性。作为 Node.js 开发者,拥抱 Fastify 意味着你可以用更少的代码实现更安全、更快速的服务。从今天开始,尝试将你的下一个 API 项目迁移到 Fastify,你将感受到它带来的巨大优势。