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 再补一堆中间件的路径更短。


参考文档

本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。

延伸阅读

获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

在线开通,按量计费,真正的云服务!

立即开始

选择观测云版本

代码托管平台