第9章 9.4 现代框架:Fastify / Nest.js
「上一章我们把应用塞进了 Docker、用 PM2 管好了进程、再用 Nginx 绑上了域名,终于算是把服务跑起来了。但不知道你有没有这种感觉:写业务逻辑的时候,总觉得原生的 Node.js API 有点太『裸』了——路由要自己写、参数校验要自己来、测试要自己搭...有没有一种更省心的写法?」
「这一章我们要聊的就是这件事:用什么框架能让我们写代码更快、更稳、更舒服?Fastify 快在哪里?Nest.js 的模块化和 Angular 血缘又是什么意思?学完之后,你就能自己挑一个顺手的框架来用了。」
🎯 开场 3 分钟:为什么要学这个?
场景带入:当你写一个用户注册接口
假设你要写一个用户注册的 API,需要:
- 接收
username、email、password三个参数 - 校验
email格式对不对 - 校验
password长度是不是 ≥ 6 位 - 把数据存进数据库
- 返回注册成功还是失败
用原生 Node.js 写,你需要大概 80 行代码,手动解析 request.body,手动写一堆 if 判断。
用框架写,大概 20 行,而且更安全、更容易测试。
这就是框架的价值——帮你把重复的脏活干了,让你专注业务逻辑。
痛点问题
- 原生
http模块写路由,全靠if...else判断路径,代码一多就变成「意大利面条」 - 参数校验、错误处理、日志记录...每写一个接口都要重复这套代码
- 测试不知道怎么写,改了代码不知道怎么验证有没有 bug
学完能解决什么
- 能根据项目需求选择合适的框架(Fastify 快、NestJS 架构好)
- 能用框架快速搭建一个带路由、有校验、可测试的 API 服务
- 能看懂框架的文档,自己写插件、扩展功能
🧱 基础 25 分钟:核心概念
概念一:什么是框架?——「外卖平台 vs 自己做饭」
没用框架 = 你自己买菜、洗菜、切菜、炒菜、装盘
用了框架 = 外卖平台,你只管点菜,后厨帮你全干了
框架就是别人帮你搭好的「脚手架」,你只需要往里面填业务逻辑,不用从零盖房子。
概念二:Fastify 是什么?——「高铁头等舱」
Fastify 号称是「Node.js 里最快的 HTTP 框架」。快的原因主要是两点:
- ** schema 先行**:提前告诉 Fastify 你的数据长什么样,它用更高效的方式处理
- 轻量级:没那么多花里胡哨的东西,执行效率高
类比一下:Fastify 就像高铁——线路固定(约定优于配置)、速度快(性能优先)、服务实在(插件丰富)。
概念三:NestJS 是什么?——「装修好的公寓」
NestJS 是一个「类 Angular」的 Node.js 框架,最大的特点是模块化。
如果说 Fastify 是「高铁」,那 NestJS 就是「精装公寓」——它把什么都给你安排好了:路由、依赖注入、ORM 集成、测试工具...你只需要按照它的「装修风格」往里填家具。
适合做大型企业项目,多人协作时有统一规范。
概念四:路由和处理器——「挂号台和医生」
路由(Route)= 医院的挂号台,负责接收请求、问清楚你要看什么科
处理器(Handler)= 医生,真正给你看病的地方
用户请求 → 路由匹配 → 找到对应处理器 → 返回结果
概念五:中间件——「安检通道」
每个请求都要过安检:检查你有没有带危险品(参数校验)、你的身份对不对(登录态检查)...
中间件就是这些安检人员,请求在到达最终处理器之前,先经过中间件的层层把关。
快速上手 Fastify
先安装:
npm install fastify
写一个最简单的服务:
// 01-hello-fastify.js
const fastify = require('fastify')({ logger: true })
// 定义一个路由:GET / 返回 "Hello, Fastify!"
fastify.get('/', async (request, reply) => {
return 'Hello, Fastify!'
})
// 启动服务
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 01-hello-fastify.js
打开浏览器访问 http://localhost:3000,应该能看到 Hello, Fastify!。
这 15 行代码做的事:
- 创建了一个 Fastify 实例
- 定义了一个 GET 路由 /
- 启动了服务监听 3000 端口

带参数的路由
// 02-route-params.js
const fastify = require('fastify')({ logger: true })
// GET /hello/:name 表示 name 是一个路径参数
fastify.get('/hello/:name', async (request, reply) => {
const { name } = request.params // 从 params 里取到 name
return `你好,${name}!`
})
const start = async () => {
try {
await fastify.listen({ port: 3000 })
} catch (err) {
fastify.log.error(err)
process.exit(1)
}
}
start()
访问 http://localhost:3000/hello/小明,页面会显示 你好,小明!
request.params 就是 URL 路径里的变量,比如 /hello/世界 中 name = '世界'。
处理 POST 请求和请求体
// 03-post-json.js
const fastify = require('fastify')({ logger: true })
// 定义 POST /register 接口,接收 JSON body
fastify.post('/register', async (request, reply) => {
const { username, email, password } = request.body
// 简单校验
if (!username || !email || !password) {
reply.code(400) // 返回 400 错误码
return { error: '所有字段都不能为空' }
}
if (password.length < 6) {
reply.code(400)
return { error: '密码至少要 6 位' }
}
// 模拟注册成功
return {
success: true,
message: `用户 ${username} 注册成功!`,
data: { username, email }
}
})
const start = async () => {
await fastify.listen({ port: 3000 })
}
start()
用 curl 测试:
curl -X POST http://localhost:3000/register \
-H "Content-Type: application/json" \
-d '{"username":"张三","email":"zhangsan@test.com","password":"123456"}'
会返回:
{"success":true,"message":"用户 张三 注册成功!","data":{"username":"张三","email":"zhangsan@test.com"}}
NestJS 快速体验
NestJS 用 TypeScript 写的,语法更「正经」,适合大项目。先感受一下它的风格:
npm i -g @nestjs/cli
nest new my-nest-app # 会问你要不要用 npm,选是就行
cd my-nest-app
npm run start:dev
NestJS 的核心概念是模块(Module)、控制器(Controller)、服务(Service):
模块 = 一个大箱子,装相关的控制器和服务
控制器 = 负责接收请求,决定谁来处理
服务 = 真正干活的业务逻辑

如果你熟悉 Angular,学 NestJS 会觉得特别亲切;如果没学过也没关系,记住这个「箱子-挂号台-医生」的比喻就够了。
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):一个带分页的博客列表 API
跟着抄就能跑:
// 04-blog-list.js
const fastify = require('fastify')({ logger: true })
// 模拟数据库里的文章数据
const articles = [
{ id: 1, title: 'Fastify 入门', author: '老王', views: 1200 },
{ id: 2, title: 'NestJS 进阶', author: '小李', views: 980 },
{ id: 3, title: 'Docker 部署实战', author: '老王', views: 2100 },
{ id: 4, title: 'PM2 进程管理', author: '小张', views: 860 },
{ id: 5, title: 'Nginx 反向代理', author: '小李', views: 1500 }
]
// GET /articles?page=1&limit=3
fastify.get('/articles', async (request, reply) => {
const { page = 1, limit = 3 } = request.query
const pageNum = parseInt(page)
const limitNum = parseInt(limit)
const start = (pageNum - 1) * limitNum
const end = start + limitNum
const paginatedArticles = articles.slice(start, end)
return {
page: pageNum,
limit: limitNum,
total: articles.length,
data: paginatedArticles
}
})
const start = async () => {
await fastify.listen({ port: 3000 })
}
start()
预期输出(访问 http://localhost:3000/articles?page=1&limit=2):
{
"page": 1,
"limit": 2,
"total": 5,
"data": [
{"id": 1, "title": "Fastify 入门", "author": "老王", "views": 1200},
{"id": 2, "title": "NestJS 进阶", "author": "小李", "views": 980}
]
}
解释:这个接口帮你实现了「分页」,page 是第几页,limit 是每页几条。
项目 2(15 分钟):读取 JSON 文件,返回过滤后的数据
这个项目演示怎么结合文件系统操作,做一个「文章搜索过滤」的功能。
准备一个 articles.json 文件:
[
{"id": 1, "title": "Node.js 是什么", "category": "基础", "views": 5000},
{"id": 2, "title": "Express 框架入门", "category": "框架", "views": 3200},
{"id": 3, "title": "Fastify 性能优化", "category": "进阶", "views": 1800},
{"id": 4, "title": "NestJS 模块化设计", "category": "架构", "views": 2100},
{"id": 5, "title": "Docker 从入门到实战", "category": "DevOps", "views": 8900},
{"id": 6, "title": "PM2 进程管理", "category": "DevOps", "views": 2500}
]
写一个服务,支持按分类筛选 + 按浏览量排序:
// 05-file-filter.js
const fastify = require('fastify')({ logger: true })
const fs = require('fs').promises
// 读取 JSON 文件的辅助函数
async function loadArticles() {
const data = await fs.readFile('articles.json', 'utf-8')
return JSON.parse(data)
}
// GET /articles?category=DevOps&sort=views&order=desc
fastify.get('/articles', async (request, reply) => {
const { category, sort = 'id', order = 'asc' } = request.query
let articles = await loadArticles()
// 按分类过滤
if (category) {
articles = articles.filter(a => a.category === category)
}
// 排序
articles.sort((a, b) => {
const valA = a[sort]
const valB = b[sort]
if (order === 'desc') {
return valB - valA // 降序
}
return valA - valB // 升序
})
return {
total: articles.length,
filters: { category, sort, order },
data: articles
}
})
const start = async () => {
await fastify.listen({ port: 3000 })
}
start()
测试一下:
# 筛选 DevOps 分类,按浏览量降序
curl "http://localhost:3000/articles?category=DevOps&sort=views&order=desc"
预期输出:
{
"total": 2,
"filters": {"category": "DevOps", "sort": "views", "order": "desc"},
"data": [
{"id": 5, "title": "Docker 从入门到实战", "category": "DevOps", "views": 8900},
{"id": 6, "title": "PM2 进程管理", "category": "DevOps", "views": 2500}
]
}
解释:读文件 → 按条件过滤 → 排序 → 返回。核心就是 fs.readFile + 数组的 filter + sort。
项目 3(15 分钟):做一个「文章统计」小工具
组合前两个项目的能力,做一个统计文章数据的工具:
// 06-stats.js
const fastify = require('fastify')({ logger: true })
const fs = require('fs').promises
async function loadArticles() {
const data = await fs.readFile('articles.json', 'utf-8')
return JSON.parse(data)
}
// GET /stats 返回统计摘要
fastify.get('/stats', async (request, reply) => {
const articles = await loadArticles()
// 总文章数
const totalCount = articles.length
// 总浏览量
const totalViews = articles.reduce((sum, a) => sum + a.views, 0)
// 各分类文章数量
const categoryCount = {}
articles.forEach(a => {
categoryCount[a.category] = (categoryCount[a.category] || 0) + 1
})
// 浏览量最高的 3 篇文章
const topByViews = [...articles]
.sort((a, b) => b.views - a.views)
.slice(0, 3)
.map(a => ({ title: a.title, views: a.views }))
// 平均浏览量
const avgViews = Math.round(totalViews / totalCount)
return {
summary: {
totalArticles: totalCount,
totalViews,
avgViews,
categoryCount
},
topArticles: topByViews
}
})
// GET /stats/category/:name 返回某个分类的统计
fastify.get('/stats/category/:name', async (request, reply) => {
const { name } = request.params
const articles = await loadArticles()
const filtered = articles.filter(a => a.category === name)
if (filtered.length === 0) {
reply.code(404)
return { error: `没有找到分类 "${name}" 的文章` }
}
const totalViews = filtered.reduce((sum, a) => sum + a.views, 0)
return {
category: name,
articleCount: filtered.length,
totalViews,
avgViews: Math.round(totalViews / filtered.length)
}
})
const start = async () => {
await fastify.listen({ port: 3000 })
}
start()
测试:
# 全局统计
curl http://localhost:3000/stats
# 某个分类的统计
curl http://localhost:3000/stats/category/DevOps
全局统计预期输出:
{
"summary": {
"totalArticles": 6,
"totalViews": 23400,
"avgViews": 3900,
"categoryCount": {
"基础": 1,
"框架": 1,
"进阶": 1,
"架构": 1,
"DevOps": 2
}
},
"topArticles": [
{"title": "Docker 从入门到实战", "views": 8900},
{"title": "Node.js 是什么", "views": 5000},
{"title": "Express 框架入门", "views": 3200}
]
}
解释:统计工具的精髓就是「分组 + 聚合」——把所有文章按条件分组,算出每组的数量、总浏览量、平均值。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:异步错误没 catch,服务直接崩了
❌ 错误写法:
fastify.get('/unsafe', async (request, reply) => {
// 如果 JSON.parse 失败了,整个服务会崩
const data = JSON.parse(undefinedVar)
return data
})
✅ 正确写法:
fastify.get('/safe', async (request, reply) => {
try {
const data = JSON.parse(undefinedVar)
return data
} catch (err) {
reply.code(500)
return { error: '数据处理出错', message: err.message }
}
})
解释:Node.js 里没捕获的异步错误会导致进程崩溃。用 try...catch 包住所有可能出错的地方。
坑 2:路由参数和查询参数傻傻分不清
/users/:id→request.params.id(路径参数,如/users/123)/search?q=keyword→request.query.q(查询参数,URL 后面那个?)
❌ 错误:request.body.q 去拿 URL 参数
✅ 正确:路径参数用 request.params,查询参数用 request.query
坑 3:修改了数组没声明,Vue/React 拿不到新值
这个是前端同学常踩的坑。如果你的 API 返回的是从数据库查出来的数组:
// ❌ 错误:直接返回 articles,然后在外面修改了它
const articles = await db.findAll()
return articles // 前端看到的是旧数据
// ✅ 正确:返回副本
return [...articles] // 前端拿到的是新数据
坑 4:生产环境没设 prefix,接口路径乱了
部署到 Nginx 反向代理后面时,建议给 Fastify 设一个 prefix:
const fastify = require('fastify')({
logger: true,
prefix: '/api' // 所有路由自动加 /api 前缀
})
这样你的 /users 路由实际是 /api/users,和前端约定的接口路径更规范。
坑 5:日志开太详细,生产环境性能暴跌
❌ 开发模式:每条请求都打印详细日志,包括 body、headers...
✅ 生产模式:关掉详细日志,只打印错误
const fastify = require('fastify')({
logger: process.env.NODE_ENV === 'production' ? false : true
})
性能小贴士:善用 fast-json-stringify 提速 JSON 序列化
Fastify 默认的 JSON 序列化已经很快了,但如果你的接口 QPS 特别高(每秒几万次请求),可以用 fast-json-stringify 进一步加速:
npm install fast-json-stringify
const fastify = require('fastify')()
const fastJson = require('fast-json-stringify')
// 定义 schema,序列化速度翻倍
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' }
}
}
const serialize = fastJson(schema)
fastify.get('/user', (request, reply) => {
const user = { name: '张三', age: 25 }
reply.send(serialize(user))
})
调试技巧:开启 Fastify 的详细日志
const fastify = require('fastify')({
logger: {
level: 'debug', // 改成 debug 能看到每条请求的详细信息
transport: {
target: 'pino-pretty', // 格式化输出,更好看
options: { colorize: true }
}
}
})
✏️ 练习题
练习 1(2 分钟):改个路由路径
在项目 1 的基础上,把 /articles 改成 /posts,同时把返回字段 title 改成 post_title。
// 原代码
fastify.get('/articles', async (request, reply) => { ... })
// 改完应该是
fastify.get('/posts', async (request, reply) => { ... })
练习 2(2 分钟):加一个判断
在项目 2 的 /register 接口里,加一个判断:如果 username 长度小于 3 个字符,返回错误 "用户名至少要 3 个字符"。
练习 3(3 分钟):换个字段排序
用项目 2 的代码,试试按 views 升序排列(最不受欢迎的文章排前面)。只需要改 URL 参数。
# 预期:views 最少的排前面
curl "http://localhost:3000/articles?sort=views&order=asc"
练习 4(3 分钟):把两个项目串起来
在项目 3 的 /stats 接口里加一个新功能:支持 ?category=DevOps 参数,只统计指定分类的数据。如果没有传 category,就统计所有文章。
练习 5(挑战题,5 分钟):分析这个报错
你运行下面这段代码,浏览器访问 http://localhost:3000/search?q=test,页面报错 Cannot read properties of undefined (reading 'length')。
fastify.get('/search', async (request, reply) => {
const query = request.body.q // 错在哪里?
return { result: query.length > 0 ? query : '没有关键词' }
})
问题在哪?怎么改?
📚 总结 + 资源
本文学到的 3 件事:
- 框架帮你省脏活:路由、参数校验、中间件这些重复的事,框架都帮你封装好了
- Fastify 快在 schema + 轻量:提前告诉框架数据结构,它用最省力的方式处理
- NestJS 适合大项目:模块化、依赖注入、多人协作时优势明显
延伸学习资源:
- Fastify 官方文档:https://www.fastify.cn/ (中文!新手友好)
- NestJS 官方文档:https://docs.nestjs.com/ (英文,但例子很清晰)
- 《Node.js 设计模式》- 进阶必读,理解框架背后的原理
互动钩子:
「你现在写的 Node.js 项目,是用原生 http 还是在用框架?用过 Fastify 或 NestJS 的同学,来说说感受呗,老粉优先回复!」
「学会了框架怎么用,下一章我们就来玩点更酷的——实时聊天系统。基于 WebSocket,服务端可以主动往客户端发消息,不用等客户端轮询。想做一个像微信一样能实时聊天的 App 吗?下一章见!」

评论(0)