Nest 内部并没有直接依赖任何一个 http 处理的库,只是依赖了抽象的接口,想用什么库需要实现这些接口的适配器
express 是什么
Express 是 Node.js 生态里最早也是最广为人知的 Web 框架,2010 年发布,作者 TJ Holowaychuk。它做的事情很简单:
- 包了一层 Node 自带的
http模块 - 提供路由(
app.get('/users', handler)) - 提供中间件机制(
app.use(middleware)) - 提供
req/res上的常用扩展方法(res.json()、res.status()等)
一个最小的 Express 应用:
1
2
3
4
5
6
7
8
const express = require("express");
const app = express();
app.get("/hello", (req, res) => {
res.json({ message: "hello" });
});
app.listen(3000);
特点:
- 极简、非常自由:没有约定目录结构,没有依赖注入,怎么组织代码都行
- 生态庞大:几乎所有 Node Web 中间件(
body-parser、cors、helmet、morgan…)都是为它写的 - 缺点:没有规范,团队规模一大就容易写成”面条代码”;TypeScript 支持也不是原生的
可以把 Express 理解成 “乐高底座”——给你最基础的拼接能力,但盖什么样的房子全靠自己。
nestjs 和 express 之间的关系
很多人第一次看到 NestJS 会以为它是 Express 的”竞品”,其实两者不是同一层的东西:
NestJS 默认就是跑在 Express 之上的。
Nest 自己不实现 HTTP 服务器,它做的是在 Express(或 Fastify)之上再封装一层”框架级”的东西:模块化(Module)、依赖注入(DI)、装饰器路由(@Controller / @Get)、AOP(Guard / Interceptor / Filter)等等。
可以画个分层图:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────────────────┐
│ 你的业务代码 (Controller / Service) │
├─────────────────────────────────────────┤
│ NestJS 框架层 │
│ - DI 容器、装饰器、模块系统 │
│ - Guard / Interceptor / Filter / Pipe │
├─────────────────────────────────────────┤
│ HTTP 适配层 (Adapter) │
│ ├── @nestjs/platform-express ← 默认 │
│ └── @nestjs/platform-fastify │
├─────────────────────────────────────────┤
│ Express / Fastify │
├─────────────────────────────────────────┤
│ Node.js http 模块 │
└─────────────────────────────────────────┘
关键事实:
- Nest 默认底层就是 Express:你
npm i @nestjs/core时会自动带上@nestjs/platform-express - 可以拿到原生 Express 实例:通过
app.getHttpAdapter().getInstance()能拿到 Express app,挂任意 Express 中间件都没问题 req/res就是 Express 的对象:在@Req()/@Res()拿到的就是 Express 的Request/Response- Express 中间件直接复用:
cors、helmet、morgan等用app.use(...)直接挂
举个例子,挂一个 Express 中间件:
1
2
3
4
5
6
7
8
9
10
11
// main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import * as cors from "cors";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(cors()); // ← 这里的 use 就是 Express 的 use
await app.listen(3000);
}
bootstrap();
类比一下:
| 角色 | Express | NestJS |
|---|---|---|
| 定位 | HTTP 框架(底层) | 应用框架(上层) |
| 风格 | 函数式、自由 | OOP + 装饰器、规范 |
| 关系 | — | 默认跑在 Express 上 |
| 类比 | 乐高底座 | 用乐高搭好的”半成品别墅” |
一句话总结:Nest 不是替代 Express,而是把 Express “包装”成一个更适合中大型项目的工程化框架。
如何切到 fastify
为什么要切到 Fastify?因为它比 Express 快(官方 benchmark 大约快 2 倍)、原生支持 schema validation、对 TypeScript 更友好。
切换非常简单,3 步搞定:
1. 安装 Fastify 适配包
1
2
3
pnpm add @nestjs/platform-fastify
# 或
npm i @nestjs/platform-fastify
2. 在 main.ts 里换适配器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// main.ts
import { NestFactory } from "@nestjs/core";
import {
FastifyAdapter,
NestFastifyApplication,
} from "@nestjs/platform-fastify";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(), // ← 关键:传入 Fastify 适配器
);
await app.listen(3000, "0.0.0.0");
}
bootstrap();
注意 NestFactory.create 的泛型参数也要换成 NestFastifyApplication,这样 app 上的 getHttpAdapter() 等方法的类型才正确。
3. 替换 Express-only 的中间件 / 写法
大部分代码不用动,但有几个地方要小心:
| Express 写法 | Fastify 替代 |
|---|---|
app.use(cors()) |
pnpm add @fastify/cors + app.register(...) |
app.use(helmet()) |
pnpm add @fastify/helmet + app.register(...) |
@Res() res: Response(Express 类型) |
@Res() res: FastifyReply(Fastify 类型) |
req.cookies |
pnpm add @fastify/cookie + app.register(...) |
静态资源 app.useStaticAssets |
pnpm add @fastify/static |
举例:
1
2
3
4
5
6
7
import fastifyCors from "@fastify/cors";
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.register(fastifyCors, { origin: "*" });
切换前的检查清单
切到 Fastify 之前,过一下这几条:
- 是否依赖了 Express-only 的中间件:
express-session、passport的某些 strategy、multer等。Fastify 都有对应替代(@fastify/session、fastify-passport、@fastify/multipart),但要逐个替换 - 代码里有没有直接用 Express 类型:
Request/Response来自express,要换成FastifyRequest/FastifyReply @Res()的使用:尽量避免直接操作res,用 Nest 的返回值机制(return data)就跨平台无感- 第三方库兼容性:比如 Swagger 模块两端都支持;但少数老库可能只兼容 Express
QA: 一定要切 Fastify 吗?
💬点击展开/收起
不一定。考虑这几点再决定:
- 性能瓶颈在哪:大多数业务系统的瓶颈是数据库 / 网络 IO,HTTP 框架带来的差异微乎其微。切 Fastify 不会让”慢接口”变快
- 生态成本:Express 中间件生态比 Fastify 大一截,迁移要逐个替换
- 团队熟悉度:调试问题、看错误栈,Express 资料更多
建议:
- 新项目、追求极致性能 / 想用 schema validation → 直接上 Fastify
- 老项目、依赖一堆 Express 中间件、性能不是瓶颈 → 留在 Express,不折腾
QA: 为什么 Nest 要做能够轻松切换 http 处理库呢?
💬点击展开/收起
这其实是 NestJS 设计哲学里很关键的一点——面向接口编程,而不是面向具体实现。具体有这几个动机:
1. 避免被单一框架绑死(防供应商锁定)
Express 已经多年没有大版本更新,社区一度担心它停滞;Fastify、Hyper-Express、uWebSockets 等新库在性能、特性上不断迭代。如果 Nest 把 Express 写死在内核里,一旦底层换代,整个 Nest 生态都得跟着大改。
抽象出 HttpAdapter 接口之后,Nest 自己只依赖”一个能注册路由、能写响应”的契约,谁实现这个契约都行。Express 老了换 Fastify、Fastify 老了换下一个,都不影响业务代码。
2. 让用户按场景选最合适的引擎
不同场景需求不同:
- CRUD 后台:Express 生态完整、调试方便 → 用 Express
- 高并发网关 / BFF:QPS 敏感 → 用 Fastify(快约 2 倍)
- 极致低延迟:考虑 uWebSockets 适配器
- Serverless:用专门优化冷启动的适配器
如果框架强绑定一个 HTTP 库,开发者要么忍受不合适的性能,要么放弃整个框架。
3. 真正的”协议无关”AOP
这是和 NestJS 自身定位连在一起的:Nest 不只是”HTTP 框架”,它还要支持 WebSocket、TCP/Redis/NATS/Kafka 微服务、GraphQL 等多种协议。HTTP 只是其中一种。既然要做”协议无关”的 Guard / Interceptor / Filter(通过 ArgumentsHost / ExecutionContext 抽象),那 HTTP 这一层自然也要可替换,不能耦合到 Express。
4. 便于测试
抽象出 Adapter 之后,单元测试可以传一个 Mock Adapter 进去,不必真的起一个 HTTP 服务器,启动速度和稳定性都好很多。
5. 一句话总结
Nest 不想做 Express 的”上层语法糖”,它想做一个协议无关的应用框架。HTTP 库可换、传输协议可换,业务代码(Controller / Service)保持不变——这才是它和”裸 Express + 一堆约定”的本质区别。
QA: Nest 的中间件和 Express 中间件有什么区别?
💬点击展开/收起
结论先行:Nest 中间件底层就是 Express 中间件(在 Express 适配器下),但被 Nest 包装了一层,让它能享受 DI、装饰器、模块系统。
1. 函数式中间件——和 Express 完全一样
最简单的写法和 Express 没区别:
1
2
3
4
5
6
7
// logger.middleware.ts
import { Request, Response, NextFunction } from "express";
export function logger(req: Request, res: Response, next: NextFunction) {
console.log(`[${req.method}] ${req.url}`);
next();
}
2. 类式中间件——Nest 才有的写法
类式中间件可以注入其他 service,享受完整的 DI 能力:
1
2
3
4
5
6
7
8
9
10
11
12
13
// logger.middleware.ts
import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
constructor(private readonly logService: LogService) {} // ✅ 可以注入 service
use(req: Request, res: Response, next: NextFunction) {
this.logService.write(`${req.method} ${req.url}`);
next();
}
}
3. 注册方式不同
| 维度 | Express | Nest |
|---|---|---|
| 注册位置 | app.use(mw) |
Module 里 configure(consumer) 中 consumer.apply() |
| 路由匹配 | 全局或 app.use('/api', mw) |
.forRoutes('users') 或 .forRoutes(UsersController) |
| 排除路由 | 自己写判断 | .exclude({ path: 'auth/login', method: ... }) |
| DI 支持 | 没有 | 类式中间件可注入 |
| 多个中间件 | app.use(a, b, c) |
consumer.apply(a, b, c) |
1
2
3
4
5
6
7
8
9
10
// app.module.ts
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.exclude({ path: "health", method: RequestMethod.GET })
.forRoutes(UsersController); // 只对 UsersController 生效
}
}
4. 执行时机:中间件 → Guard → Interceptor → Pipe → Handler
中间件是最早执行的层(紧贴 HTTP 库),所以拿到的还是裸的 req / res,没有 Nest 的请求上下文。需要读 @Roles() 之类的元数据,用 Guard 不要用中间件。
5. 一句话总结
- 函数式中间件 = Express 中间件(兼容性写法)
- 类式中间件 = Express 中间件 + DI + 装饰器友好的注册方式
- 切到 Fastify 后,类式中间件依然能用,只是
req/res类型要换成 Fastify 的
QA: Nest 的 @Module 和 Express 的 Router 是同一个东西吗?
💬点击展开/收起
不是。 两者解决的问题不同,只是表面上”都能拆分代码”,但定位完全不同。
Express 的 Router:纯路由组合工具
1
2
3
4
5
6
7
8
// users.routes.js
const router = express.Router();
router.get("/", listUsers);
router.post("/", createUser);
module.exports = router;
// app.js
app.use("/users", require("./users.routes"));
它做的事就一件——把一组路由打包成可挂载的子树。仅此而已。
Nest 的 @Module:DI 容器的边界 + 路由 + 生命周期
1
2
3
4
5
6
7
@Module({
imports: [TypeOrmModule.forFeature([User])], // 引入其他模块的能力
controllers: [UsersController], // 路由(≈ Router 的部分)
providers: [UsersService, UserRepository], // 本模块的可注入服务
exports: [UsersService], // 哪些 provider 给别的模块用
})
export class UsersModule {}
它至少承担四种职责:
- DI 作用域边界:
UsersService默认只在UsersModule内可见,要给别的模块用必须显式exports - 依赖关系图节点:
imports形成模块依赖图,Nest 据此决定实例化顺序和单例共享范围 - 路由组合(这部分才和 Express Router 重合):
controllers里的@Controller('users')等价于app.use('/users', router) - 生命周期钩子:模块可实现
OnModuleInit/OnApplicationBootstrap等,启动时按依赖顺序触发
对比表
| 维度 | Express Router | Nest @Module |
|---|---|---|
| 主要职责 | 路由组合 | DI 边界 + 路由 + 生命周期 |
| 依赖管理 | 无 | imports / exports 显式声明 |
| 实例隔离 | 无(全局共享) | 每个 module 是独立的 DI scope |
| 子级能用什么 | 父级 app 的所有东西 |
只能用本模块定义 + import 进来 + 全局模块的 |
| 生命周期 | 无 | OnModuleInit / OnDestroy 等 |
| 类比 | 文件夹 | 一个微型”应用单元” |
举个例子说明差异
1
2
3
4
5
6
7
8
9
10
// OrderService 只在 OrderModule 里被声明
@Module({ providers: [OrderService] })
export class OrderModule {}
// UserModule 没 import OrderModule 就用不了 OrderService
@Module({
// imports: [OrderModule], ← 没写就报错
providers: [UserService], // UserService 里 inject OrderService 会失败
})
export class UserModule {}
Express 不会有这种”看不见”的限制——任何文件 require 一下就用了。Nest 用 @Module 强制你显式声明依赖,换来的是清晰的边界、可测试性、和大型项目的可维护性。
一句话总结:Express Router 是”路由的子文件夹”;Nest @Module 是”一个迷你应用 + 它管的路由”,更接近 Java Spring 的 @Configuration / Angular 的 NgModule。
小结
- Express 是 Node.js 上的极简 HTTP 框架,提供路由 + 中间件,自由度高但缺乏约束
- NestJS 默认就是跑在 Express 之上的应用框架,提供 DI、装饰器、模块化等工程化能力;它不是 Express 的替代品,而是包装
- 切到 Fastify 只需替换适配器(
new FastifyAdapter())+ 把用到的 Express 中间件换成@fastify/*系列,业务代码基本不用动 - 是否切换取决于性能需求和生态依赖,不要为了切而切