Skip to content

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 获取,这是构造函数注入的前提。如果使用普通函数,框架无法自动推断其依赖。

typescript
// ✅ 类:框架可自动注入 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 的问题:

typescript
class UserService {
  // 硬编码依赖:更换数据库驱动需要修改所有 new 的地方
  private repo = new UserRepository(new PostgresConnection('localhost:5432'));
}

有 DI 的好处:

typescript
@Injectable()
class UserService {
  constructor(private repo: UserRepository) {}
  // 测试时可注入 MockUserRepository,生产时注入真实实现
  // 切换数据库只需修改 Module 配置,Service 代码不变
}

三大好处:

  • 解耦:组件只依赖接口/token,不依赖实现细节
  • 可测试:单元测试时注入 Mock,无需真实数据库
  • 可复用:同一个 Provider 实例在整个应用中共享(默认单例)

3. 装饰器驱动

@Controller()@Get()@Injectable() 等装饰器将元信息附加到类上,框架在启动时读取这些元信息来组装应用。

这种方式让业务代码与框架代码分离

typescript
// 业务逻辑在方法体内,框架关注点(路由、守卫)在装饰器里
@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 声明依赖关系。

模块化解决了大型应用的三个问题:

  1. 边界清晰UserModule 内部的 Provider 对外不可见,除非显式 exports
  2. 可独立测试:每个 Module 可以单独创建测试上下文
  3. 可懒加载:使用 LazyModuleLoader 按需加载不常用的 Module

三、与 Express 的关系

NestJS 默认基于 Express(也可切换为 Fastify),但它并不替代 Express——它是架构层,Express 是传输层。

请求 → Express(HTTP 解析、路由分发)

      NestJS(模块系统、DI、生命周期钩子)

      Controller → Service → Repository

NestJS 在 Express 之上添加了:

  • 模块系统(Module)
  • 依赖注入容器(DI Container)
  • 请求生命周期钩子(Guard、Interceptor、Pipe、ExceptionFilter)
  • 装饰器元数据驱动的路由注册

切换到 Fastify

typescript
// 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 数组中,或依赖的模块没有 exports
  • A circular dependency detected → A 依赖 B,B 又依赖 A,需要用 forwardRef()

六、与其他框架的对比

维度NestJSExpressFastifyKoa
学习曲线较高(需理解 DI、装饰器)
架构约束
TypeScript 支持原生需配置良好需配置
测试友好度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
大型团队协作⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
微服务支持内置需自行集成需自行集成需自行集成

结论: 团队规模越大、业务越复杂,NestJS 的约束越有价值。小型项目的"过度设计"在大型项目中变成"必要规范"。


可运行 Demo: practice/01-ioc-demo — 手动实现 IoC 容器,理解 reflect-metadata 工作原理


常见错误

错误原因解决
服务未注入,调用时为 undefined忘记在 providers 中注册检查 @Module({ providers: [MyService] })
循环依赖导致启动报错A 依赖 B,B 依赖 AforwardRef(() => B) 打破循环
模块间 Service 无法注入目标模块未 export 该 Service在提供方模块加 exports: [MyService]

NestJS 深度学习体系