第9章 9.4 现代框架:Fastify / Nest.js

「上一章我们把应用塞进了 Docker、用 PM2 管好了进程、再用 Nginx 绑上了域名,终于算是把服务跑起来了。但不知道你有没有这种感觉:写业务逻辑的时候,总觉得原生的 Node.js API 有点太『裸』了——路由要自己写、参数校验要自己来、测试要自己搭...有没有一种更省心的写法?」

「这一章我们要聊的就是这件事:用什么框架能让我们写代码更快、更稳、更舒服?Fastify 快在哪里?Nest.js 的模块化和 Angular 血缘又是什么意思?学完之后,你就能自己挑一个顺手的框架来用了。」


🎯 开场 3 分钟:为什么要学这个?

场景带入:当你写一个用户注册接口

假设你要写一个用户注册的 API,需要:

  1. 接收 usernameemailpassword 三个参数
  2. 校验 email 格式对不对
  3. 校验 password 长度是不是 ≥ 6 位
  4. 把数据存进数据库
  5. 返回注册成功还是失败

用原生 Node.js 写,你需要大概 80 行代码,手动解析 request.body,手动写一堆 if 判断。

用框架写,大概 20 行,而且更安全、更容易测试。

这就是框架的价值——帮你把重复的脏活干了,让你专注业务逻辑

痛点问题

  • 原生 http 模块写路由,全靠 if...else 判断路径,代码一多就变成「意大利面条」
  • 参数校验、错误处理、日志记录...每写一个接口都要重复这套代码
  • 测试不知道怎么写,改了代码不知道怎么验证有没有 bug

学完能解决什么

  • 能根据项目需求选择合适的框架(Fastify 快、NestJS 架构好)
  • 能用框架快速搭建一个带路由、有校验、可测试的 API 服务
  • 能看懂框架的文档,自己写插件、扩展功能

🧱 基础 25 分钟:核心概念

概念一:什么是框架?——「外卖平台 vs 自己做饭」

没用框架 = 你自己买菜、洗菜、切菜、炒菜、装盘
用了框架 = 外卖平台,你只管点菜,后厨帮你全干了

框架就是别人帮你搭好的「脚手架」,你只需要往里面填业务逻辑,不用从零盖房子。

概念二:Fastify 是什么?——「高铁头等舱」

Fastify 号称是「Node.js 里最快的 HTTP 框架」。快的原因主要是两点:

  1. ** schema 先行**:提前告诉 Fastify 你的数据长什么样,它用更高效的方式处理
  2. 轻量级:没那么多花里胡哨的东西,执行效率高

类比一下: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 端口

配图1 - 配图1

带参数的路由

// 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)

模块 = 一个大箱子,装相关的控制器和服务
控制器 = 负责接收请求,决定谁来处理
服务 = 真正干活的业务逻辑

配图2 - 配图2

如果你熟悉 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/:idrequest.params.id(路径参数,如 /users/123
  • /search?q=keywordrequest.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 件事

  1. 框架帮你省脏活:路由、参数校验、中间件这些重复的事,框架都帮你封装好了
  2. Fastify 快在 schema + 轻量:提前告诉框架数据结构,它用最省力的方式处理
  3. NestJS 适合大项目:模块化、依赖注入、多人协作时优势明显

延伸学习资源

  • Fastify 官方文档:https://www.fastify.cn/ (中文!新手友好)
  • NestJS 官方文档:https://docs.nestjs.com/ (英文,但例子很清晰)
  • 《Node.js 设计模式》- 进阶必读,理解框架背后的原理

互动钩子

「你现在写的 Node.js 项目,是用原生 http 还是在用框架?用过 Fastify 或 NestJS 的同学,来说说感受呗,老粉优先回复!」


「学会了框架怎么用,下一章我们就来玩点更酷的——实时聊天系统。基于 WebSocket,服务端可以主动往客户端发消息,不用等客户端轮询。想做一个像微信一样能实时聊天的 App 吗?下一章见!」

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。