Fastify 入门实战:从零构建一个博客应用
Fastify 是高性能 Node.js Web 框架:内建日志、JSON Schema 校验、插件化架构。本文以一个博客应用为主线,走完路由、静态文件、数据库、校验、钩子与错误处理全流程。
Fastify 的核心用法:const app = fastify({ logger: true }) 创建实例,app.get/post 定义路由(声明 Schema 后才校验),app.register 挂载插件。相比 Express,它自带高性能 pino 日志与编译型校验,插件生态覆盖认证/跨域/静态文件等全部常见需求。
Fastify 的设计哲学
受 Hapi 与 Express 启发,Fastify 在三个方向发力:性能(路由树与序列化预编译)、开发体验(JSON Schema 做校验,配合文档插件生成文档)、可扩展性(插件系统带作用域封装)。内建 pino 日志是杀手级特性——用 logger: true 显式启用结构化日志。
项目初始化与首页
mkdir blog && cd blog && npm init -y
npm install fastify@5 @fastify/static@8 better-sqlite3
mkdir public
以下按 CommonJS 编写,使用 Node.js 20+ 与 Fastify 5。各节片段按顺序放在同一文件,最后再启动监听。
const fastify = require('fastify');
const app = fastify({ logger: true });
app.get('/', async () => {
return { message: '欢迎来到我的博客' };
});
静态文件与数据库
// 静态文件
app.register(require('@fastify/static'), {
root: require('path').join(__dirname, 'public'),
});
// SQLite(better-sqlite3)作为示例数据库
const db = require('better-sqlite3')('blog.db');
db.exec(`CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL, content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
app.addHook('onClose', async () => db.close());
SQLite 示例使用同步 API,查询会占用事件循环;数据量和并发较大时应评估数据库与调用方式。
创建文章:Schema 校验一体
Fastify 路由定义直接内嵌 JSON Schema——校验失败自动返回 400,无需中间件:
const postSchema = {
body: {
type: 'object',
required: ['title', 'content'],
additionalProperties: false,
properties: {
title: { type: 'string', minLength: 1, maxLength: 200 },
content: { type: 'string', minLength: 1 },
},
},
};
app.post('/posts', { schema: postSchema }, async (request, reply) => {
const { title, content } = request.body;
const result = db.prepare('INSERT INTO posts (title, content) VALUES (?, ?)')
.run(title, content);
return reply.code(201).send({ id: Number(result.lastInsertRowid), title, content });
});
列表、详情、编辑与删除
app.get('/posts', async () => db.prepare('SELECT * FROM posts ORDER BY created_at DESC').all());
const idSchema = {
type: 'object', required: ['id'],
properties: { id: { type: 'integer', minimum: 1 } },
};
app.get('/posts/:id', { schema: { params: idSchema } }, async (request, reply) => {
const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(request.params.id);
if (!post) return reply.code(404).send({ error: '文章不存在' });
return post;
});
app.put('/posts/:id', { schema: { ...postSchema, params: idSchema } }, async (request, reply) => {
const { changes } = db.prepare('UPDATE posts SET title=?, content=? WHERE id=?')
.run(request.body.title, request.body.content, request.params.id);
if (!changes) return reply.code(404).send({ error: '文章不存在' });
return { id: request.params.id, title: request.body.title, content: request.body.content };
});
app.delete('/posts/:id', { schema: { params: idSchema } }, async (request, reply) => {
const { changes } = db.prepare('DELETE FROM posts WHERE id = ?').run(request.params.id);
if (!changes) return reply.code(404).send({ error: '文章不存在' });
return reply.code(204).send();
});
钩子:请求生命周期
app.addHook('onRequest', async (request) => {
request.log.info({ url: request.url }, '收到请求');
});
app.addHook('onSend', async (request, reply, payload) => {
reply.header('X-Powered-By', 'Fastify');
return payload;
});
钩子链:onRequest → preParsing → body 解析 → preValidation → preHandler → handler → preSerialization(适用的 payload)→ onSend → onResponse。认证、审计、耗时统计都挂在钩子上。
错误处理与安全加固
app.setErrorHandler((error, request, reply) => {
request.log.error(error);
const status = error.statusCode ?? 500;
reply.code(status).send({ error: status >= 500 ? '服务器内部错误' : error.message });
});
所有路由、钩子及插件注册完成后启动:
app.listen({ port: 3000 }).catch((error) => {
app.log.error(error);
process.exitCode = 1;
});
安全与性能插件三件套:@fastify/helmet(安全头)、@fastify/cors(跨域白名单)、@fastify/rate-limit(限流)。需选择兼容版本并按实际来源、额度和认证策略配置。
验证接口上线后的延迟与错误
Fastify 入门实战完成开发验证后,可选一条包含数据库或外部调用的业务路径接入观测云。按 Node.js APM 文档确认探针初始化顺序与框架兼容性,检查请求及下游 Span 是否连续,再按文档注入日志关联字段。以路由模板或稳定的操作名聚合,避免用用户 ID、动态 URL 或完整 GraphQL 查询作为高基数标签。
接入文档:Node.js APM 接入 · 日志采集 · 日志与链路关联。
常见问题(FAQ)
Q:Fastify 的 async handler 里抛错会怎样?
自动捕获并交给错误处理器(setErrorHandler),状态码取决于错误及处理器,不一定是 500——这是相比 Express 4 的重大体验改进(Express 5 才补上)。
Q:插件的作用域封装是什么意思?
register 通常创建子作用域,装饰器和钩子可被其后代继承,不会自动影响父级或兄弟作用域,形成真正的模块边界。需要全局生效时用 fastify-plugin 声明跳过封装。
Q:Fastify 适合新手吗?
适合。Schema 校验和结构化日志有框架支持,但需要声明 Schema 和启用 logger,新手从一开始就走在正确的路上——这比先学 Express 再补一堆中间件的路径更短。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。