楼层: 首页/ 软件技术/ Node.js 全栈实战/ Web 框架对照:Express / Fastify / NestJS 怎么选
13

Web 框架对照:Express / Fastify / NestJS 怎么选

Express vs Fastify vs NestJS

前面第 6 章讲了 Express,第 7 章讲了 NestJS,但你可能还是没底:到底该用哪个?Fastify 是不是"更快的 Express"?NestJS 是不是"过度设计"?这一章不讲用法,只讲选型——性能差异从哪来、三者的设计哲学差在哪、什么规模的项目配什么框架。选错框架不致命,但会把后面两年的开发体验拉低一个档次。

论三个框架其实在回答三个不同的问题

Express 回答的是:"我怎么用最少的概念写一个 HTTP 服务?"它的全部心智只有"中间件"一个概念,路由、错误处理、第三方库全靠中间件拼出来。代价是没有约束——100 个人写 Express 会写出 100 种结构。

Fastify 回答的是:"我要性能,还要接口契约。"它把"请求/响应的 JSON Schema"提到一等公民的位置,靠 schema 同时做校验 + 序列化加速 + 文档生成。它比 Express 快,主要就快在 schema 驱动的序列化上。

NestJS 回答的是:"大团队怎么保证 50 个人写出风格一致的代码?"它把 Angular 那一套(模块、依赖注入、装饰器)搬到后端,用框架强制分层。代价是概念多、代码量大,小项目会觉得杀鸡用牛刀。

一句话记忆:Express 最自由、Fastify 最快也最有契约、NestJS 最有规矩。

先看一张选型表

维度Express 5Fastify 5NestJS 11
核心抽象 中间件(req, res, next) 插件 + 生命周期钩子 模块 + 控制器 + 提供者(DI)
性能量级 基准 明显更高(通常数倍量级) 常用 Express 适配器,另可选 Fastify 适配器
TypeScript 靠 @types/express,类型一般 类型友好,schema 能推类型 原生 TS 优先,装饰器 + DI 类型体验最好
请求校验 自己接 zod / express-validator 内置 JSON Schema(Ajv),可自动生成文档 内置管道(Pipe)+ class-validator
生态 最大,任何中间件都能找到 Express 版 官方插件齐全,Express 中间件可兼容 官方生态完整,第三方偏向企业能力
学习曲线 最平缓(半小时上手) 中等(要理解封装与钩子) 最陡(模块/DI/装饰器/管道/守卫/拦截器)
适合 中小项目、内部工具、BFF、快速原型、Serverless 函数 高并发 API、对延迟敏感的服务、需要强契约与自动文档 多人协作的企业系统、需要清晰分层与长期维护

关于"性能"的提醒:框架吞吐量的基准测试数字,只能当量级参考,别当结论。真实系统的瓶颈几乎总是数据库、外部调用、序列化、不当的同步计算,而不是路由匹配。如果你只是"读一条记录返回 JSON",Express 和 Fastify 的差距可能还不到数据库耗时的一个零头;但如果你是高 QPS 网关、纯粹做转发和聚合,几千 vs 几万的 QPS 就是真金白银。

同一个接口,三种写法

需求:POST /users,校验 body(name 必填字符串、age 可选 0~150),成功返回 201

// ===== ① Express 5 ===== import express from "express"; import { z } from "zod"; const app = express(); app.use(express.json()); const CreateUser = z.object({ name: z.string().min(1), age: z.number().int().min(0).max(150).optional(), }); app.post("/users", async (req, res) => { const parsed = CreateUser.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: parsed.error.flatten() }); } const user = await createUser(parsed.data); res.status(201).json(user); // Express 5 起 async 函数里抛错会自动转给错误处理中间件,不用再 try/catch 包一层 }); app.listen(3000);
// ===== ② Fastify 5:schema 就是校验器 + 序列化器 + 文档 ===== import Fastify from "fastify"; const app = Fastify({ logger: true }); const createUserSchema = { body: { type: "object", required: ["name"], properties: { name: { type: "string", minLength: 1 }, age: { type: "integer", minimum: 0, maximum: 150 }, }, additionalProperties: false, // 多余字段直接拒绝,防原型链污染 }, response: { // 响应 schema:决定输出哪些字段 201: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, age: { type: "integer" } }, }, }, }; app.post("/users", { schema: createUserSchema }, async (request, reply) => { // request.body 已经被 schema 校验并收窄过,类型安全 const user = await createUser(request.body); reply.code(201); return user; // 返回对象,序列化交给 schema }); await app.listen({ port: 3000 });
// ===== ③ NestJS 11:装饰器 + DI + 管道 ===== // users.controller.ts import { Body, Controller, Post, HttpCode } from "@nestjs/common"; import { IsString, IsOptional, IsInt, Min, Max, MinLength, Length } from "class-validator"; export class CreateUserDto { @IsString() @MinLength(1) name: string; @IsOptional() @IsInt() @Min(0) @Max(150) age?: number; } @Controller("users") export class UsersController { // 依赖注入:UsersService 会自动送进来,不用自己 new constructor(private readonly users: UsersService) {} @Post() @HttpCode(201) create(@Body() dto: CreateUserDto) { // ValidationPipe 已经在全局校验过 dto,这里拿到的是干净数据 return this.users.create(dto); } } // main.ts 里开启全局校验(记得开 transform / whitelist,多余字段会被剥掉) // app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

Fastify 深水区:schema 为什么能提速

快Fastify 快的三个真实原因

① 序列化走了"代码生成"。Express 返回对象时用 JSON.stringify,它得在运行时反射对象的每个 key 去判断类型。Fastify 拿到响应 schema 后,用 fast-json-stringify 在启动时生成一段专门针对这个结构的序列化函数(编译期确定字段和类型),运行时只需按模板填值。这是它比 Express 快的主要原因——而不是因为路由匹配快。

② 顺手做了"字段裁剪"。响应 schema 里没声明的字段不会出现在输出里。这既省带宽,又天然防了"手滑把 password 字段返回给前端"的严重事故。

③ 校验器是编译型的。Ajv 会把 JSON Schema 编译成校验函数,而不是每来一个请求就去解释一遍 schema。

Fastify 的插件封装:作用域是最容易搞混的地方

// Fastify 的 register 会创建一个"子作用域":插件里加的东西默认不外泄 import fp from "fastify-plugin"; // ❌ 默认封装:这个装饰只在 /v1 这个作用域里可见 app.register(async (v1) => { v1.decorate("db", dbV1); v1.get("/v1/users", async () => v1.db.query("...")); }, { prefix: "/v1" }); // ✅ 用 fastify-plugin 包一层,就"打破封装",装饰会挂到父作用域上(全局可用) export default fp(async (app) => { app.decorate("db", pool); app.addHook("onClose", () => pool.end()); // 进程退出时优雅关闭连接池 }); // 生命周期钩子(按执行顺序): // onRequest → preParsing → preValidation → preHandler → handler // → preSerialization → onSend → onResponse app.addHook("preHandler", async (req, reply) => { // 认证就挂这里:校验 JWT,失败就 reply.code(401).send(...) });
框架层面的四个坑

坑一:以为"换了 Fastify 就快了"。如果你的接口里有一堆 await 串行调用数据库、或者响应体没给 schema,Fastify 的速度优势基本吃不到。先优化数据库和 N+1,再谈框架。

坑二:Express 5 的通配符路由写法变了。Express 5 升级了 path-to-regexp,旧的 app.get('*', ...)、'/files/*' 这类写法不再按老语义工作,要用具名通配(如 '/*splat')。从 Express 4 升级时,先把路由通配符全部过一遍,别等上线才发现某个路由匹配不上了。

坑三:Express 5 里 async 抛错虽然会自动进错误中间件,但"中间件顺序"还是关键。错误处理中间件必须用 4 个参数 (err, req, res, next) 且写在所有路由之后,否则它会被当成普通中间件,永远不生效。

坑四:NestJS 用 @Body() dto 不等于自动校验。必须全局注册 ValidationPipe(或 @UsePipes),否则 class-validator 的装饰器只是好看的注释——这是新手最常见的"写了校验却没生效"。

怎么定:三个问题问完就有答案

团队几个人? → 1~3 人:Express 或 Fastify
人数少的时候,"约定"靠口头就能传递,NestJS 的分层反而会让每个人写更多样板代码。
为什么:框架的约束成本是固定的,人越多摊得越薄;人少的时候约束就是纯开销。
是对外 API 还是内部 BFF? → 对外高并发 API:Fastify
Fastify 的 schema 天然成为接口契约,还能顺手生成 OpenAPI 文档,前后端对接少吵架。
为什么:schema 一次编写,校验、序列化、文档三处受益,这是 Fastify 最实在的价值。
要活三年以上、还会换人? → NestJS
新人进来照猫画虎就能写对,DI 让单元测试好写,模块边界让"这块代码该放哪"不再是争论。
为什么:长期项目的最大成本是"人换了一茬代码就失控",NestJS 用框架强制结构,正好压住这个成本。
  • 已经用 Express 且没痛点:别为了性能数字迁移,收益远小于风险。真要提速,先上 schema 校验 + 响应裁剪 + 缓存。
  • 想从 Express 迁到 Fastify:@fastify/express 可以兼容挂载 Express 中间件,能渐进迁移,不用一次性重写。
  • 想要 NestJS 的结构又舍不得 Fastify 的速度:NestJS 可以用 @nestjs/platform-fastify 把底层换成 Fastify,保留 DI 和装饰器的同时拿到性能。
记
本章小结

① Express 自由、生态最大、上手最快;Fastify schema 优先,性能与契约双赢;NestJS 强分层,适合大团队长周期。

② Fastify 的快来自 schema 驱动的序列化(fast-json-stringify)+ 编译型校验(Ajv)+ 响应字段裁剪,不是路由匹配。

③ 真实瓶颈永远在数据库与外部调用;框架选型的收益主要体现在开发效率、契约清晰度和团队一致性。

④ 两个必记的"没生效"陷阱:Express 5 路由通配符语法变了、NestJS 不注册 ValidationPipe 校验就形同虚设。

本章自测(框架选型)

1.(辨析题)Fastify 比 Express 快,主要快在哪里?请指出两个具体机制。

查看答案

答案:① 序列化:Fastify 根据响应 JSON Schema 用 fast-json-stringify 在启动时生成专用序列化代码,避免了 JSON.stringify 的运行时反射;② 校验:Ajv 把 schema 编译成校验函数,不是每次请求解释 schema。另外还有响应字段裁剪、更轻的请求/响应对象封装等因素。

2.(场景题)一个 6 人团队要做一个要维护 3 年的企业内部系统,模块多、需要权限分级、会不断有人交接。选哪个框架?为什么?

查看答案

答案:NestJS。理由不是性能,而是长期协作成本:模块/控制器/服务分层让"代码该放哪"有唯一答案;DI 让单元测试容易写;守卫(Guard)天然适合权限分级;拦截器统一处理日志与响应格式。人员轮换频繁时,框架强制的结构比个人自觉可靠得多。

3.(排错题)同事用 NestJS 写了一个创建接口,用 @Body() dto: CreateUserDto 接收数据,DTO 上写了 @IsString()。测试时传了 { age: "abc" },接口居然正常返回 201。问题在哪?

查看答案

答案:没有注册 ValidationPipe。NestJS 默认不做自动校验,class-validator 的装饰器必须在 ValidationPipe 参与时才会生效。修法是在 main.ts 里 app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })),或用 @UsePipes() 加在控制器上。顺带提醒:不开 whitelist 时多余字段会被保留,存在被塞入额外属性的风险。

4.(选型题)你要写一个高流量的"聚合网关",把 5 个下游接口拼成一个响应返回,QPS 上万、响应体不大。Express 还是 Fastify?

查看答案

答案:Fastify。这类服务的特点是"请求处理逻辑很薄、序列化与并发量占比高",正好踩在 Fastify 的优势上:schema 序列化能显著降低单请求 CPU 开销、响应裁剪减少带宽、开销更小的请求对象让高并发下内存更友好。但注意:真正的瓶颈往往在下游接口的响应时间和并发控制,网关层要配好超时、并发上限和缓存。