NestJS 设计哲学
一、背景与动机
NestJS 诞生于 2017 年,作者 Kamil Myśliwiec 的核心出发点是:Node.js 生态中缺少一个在架构层面有强约束的后端框架。
Express/Koa 灵活但无架构意见,导致团队各自为政:有人把所有逻辑写在路由回调里,有人自己封装 Service 层,有人混用 OOP 和函数式。进入中大型项目后,代码结构因人而异,难以维护。
NestJS 借鉴 Angular 的设计,将 模块化、依赖注入、装饰器 引入后端,提供一套有明确约定的架构骨架。
为什么不直接用 Spring Boot?
Java Spring Boot 是 NestJS 的主要灵感来源,但:
- Node.js 生态更轻量,启动速度快,适合 Serverless 和容器化部署
- TypeScript 开发体验更接近前端团队熟悉的技术栈
- 对于全栈团队,NestJS 允许前后端共用 DTO 类型定义
二、核心设计原则
1. OOP(面向对象编程)
NestJS 鼓励使用类而非函数,每个 Provider、Controller、Module 都是一个类。
原因:类天然支持依赖注入。
TypeScript 的类型信息可以在运行时通过 reflect-metadata 获取,这是构造函数注入的前提。如果使用普通函数,框架无法自动推断其依赖。
// ✅ 类:框架可自动注入 UserRepository
@Injectable()
export class UserService {
constructor(private readonly repo: UserRepository) {}
}
// ❌ 函数:框架无法自动注入
export function createUserService(repo: UserRepository) {
return { findAll: () => repo.find() };
}NestJS 并不排斥函数式编程——在 Service 内部的业务逻辑完全可以用函数式风格实现,但 模块边界上的组件(Controller、Service、Guard 等)必须是类。
2. DI(依赖注入)
不要 new 依赖,让容器替你管理。
没有 DI 的问题:
class UserService {
// 硬编码依赖:更换数据库驱动需要修改所有 new 的地方
private repo = new UserRepository(new PostgresConnection('localhost:5432'));
}有 DI 的好处:
@Injectable()
class UserService {
constructor(private repo: UserRepository) {}
// 测试时可注入 MockUserRepository,生产时注入真实实现
// 切换数据库只需修改 Module 配置,Service 代码不变
}三大好处:
- 解耦:组件只依赖接口/token,不依赖实现细节
- 可测试:单元测试时注入 Mock,无需真实数据库
- 可复用:同一个 Provider 实例在整个应用中共享(默认单例)
3. 装饰器驱动
@Controller()、@Get()、@Injectable() 等装饰器将元信息附加到类上,框架在启动时读取这些元信息来组装应用。
这种方式让业务代码与框架代码分离:
// 业务逻辑在方法体内,框架关注点(路由、守卫)在装饰器里
@Controller('users')
@UseGuards(JwtAuthGuard)
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}
}如果去掉装饰器,剩下的就是普通的 TypeScript 类和方法——这意味着业务逻辑本身不依赖框架,便于单元测试。
4. 模块化
每个功能封装为一个 Module,Module 之间通过 imports/exports 声明依赖关系。
模块化解决了大型应用的三个问题:
- 边界清晰:
UserModule内部的 Provider 对外不可见,除非显式exports - 可独立测试:每个 Module 可以单独创建测试上下文
- 可懒加载:使用
LazyModuleLoader按需加载不常用的 Module
三、与 Express 的关系
NestJS 默认基于 Express(也可切换为 Fastify),但它并不替代 Express——它是架构层,Express 是传输层。
请求 → Express(HTTP 解析、路由分发)
↓
NestJS(模块系统、DI、生命周期钩子)
↓
Controller → Service → RepositoryNestJS 在 Express 之上添加了:
- 模块系统(Module)
- 依赖注入容器(DI Container)
- 请求生命周期钩子(Guard、Interceptor、Pipe、ExceptionFilter)
- 装饰器元数据驱动的路由注册
切换到 Fastify
// main.ts
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.listen(3000, '0.0.0.0');Fastify 比 Express 在纯吞吐量上快约 20-30%,但 Express 的中间件生态更丰富。大多数项目选 Express 足够。
四、适用场景
| 场景 | 是否适合 NestJS | 说明 |
|---|---|---|
| 中大型 REST API | ✅ 强烈推荐 | 模块化管理复杂度 |
| 微服务架构 | ✅ 内置支持 | 多种传输层适配器 |
| GraphQL 服务 | ✅ 官方集成 | @nestjs/graphql 包 |
| WebSocket 实时服务 | ✅ 官方支持 | @nestjs/websockets |
| 小型脚本/工具 | ❌ 过度设计 | 用 Express 或纯 TS 更轻量 |
| Serverless 函数 | ⚠️ 注意冷启动 | 模块初始化有开销,可优化 |
| 纯函数式风格团队 | ⚠️ 有摩擦 | 强依赖 OOP 和类装饰器 |
五、NestJS 应用启动流程
理解启动流程有助于排查问题:
1. NestFactory.create(AppModule)
├─ 实例化 IoC 容器
├─ 递归扫描 AppModule 及其 imports
├─ 收集所有 providers、controllers
└─ 解析依赖图(循环依赖在此检测)
2. 依赖注入
├─ 按依赖顺序实例化 providers(叶子节点先)
└─ 将实例注入到依赖方构造函数
3. 生命周期钩子
├─ onModuleInit() — 所有模块初始化后
└─ onApplicationBootstrap() — 应用完全启动后
4. 路由注册
└─ 扫描 Controller 元数据,向 Express/Fastify 注册路由
5. app.listen(3000)
└─ 开始接受请求常见启动问题:
Nest can't resolve dependencies→ 某个 Provider 没有在当前模块的providers数组中,或依赖的模块没有exportsA circular dependency detected→ A 依赖 B,B 又依赖 A,需要用forwardRef()
六、与其他框架的对比
| 维度 | NestJS | Express | Fastify | Koa |
|---|---|---|---|---|
| 学习曲线 | 较高(需理解 DI、装饰器) | 低 | 低 | 低 |
| 架构约束 | 强 | 无 | 无 | 无 |
| TypeScript 支持 | 原生 | 需配置 | 良好 | 需配置 |
| 测试友好度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 大型团队协作 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐ |
| 微服务支持 | 内置 | 需自行集成 | 需自行集成 | 需自行集成 |
结论: 团队规模越大、业务越复杂,NestJS 的约束越有价值。小型项目的"过度设计"在大型项目中变成"必要规范"。
可运行 Demo:
practice/01-ioc-demo— 手动实现 IoC 容器,理解 reflect-metadata 工作原理
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
服务未注入,调用时为 undefined | 忘记在 providers 中注册 | 检查 @Module({ providers: [MyService] }) |
| 循环依赖导致启动报错 | A 依赖 B,B 依赖 A | 用 forwardRef(() => B) 打破循环 |
| 模块间 Service 无法注入 | 目标模块未 export 该 Service | 在提供方模块加 exports: [MyService] |