Skip to content
概述
NestJS 是在 Node.js 生态里对后端架构组织方式的一次重新理解。它不是 Express 或 Koa 的简单包装,而是一套在它们之上构建的架构框架——把模块化、依赖注入、分层职责这些概念带进了 Node.js 服务端开发。
在 NestJS 出现之前,Node.js 后端开发的主流方式是 Express。Express 足够灵活,路由定义简单直接,中间件机制也能覆盖大部分请求处理流程。但这种灵活性带来的代价是:当项目规模增长到几十个模块、上百个接口时,代码组织的决定权完全落在开发者手里。没有约定,没有分层约束,Controller、Service、数据访问逻辑很容易混在一起。每个团队都在发明自己的目录结构和分层方式。
NestJS 要解决的核心问题:在 TypeScript/Node.js 生态里提供一套可复现、可验证的架构范式。
基本概念
NestJS 的架构建立在三个角色的协作之上:Controller、Provider 和 Module。
Controller 负责接收 HTTP 请求并返回响应。它不处理业务逻辑,只做路由绑定和参数提取。Provider 是业务逻辑的载体,封装服务调用、数据库操作、外部 API 请求等。Module 则是聚合 Controller 和 Provider 的容器,同时声明自己依赖哪些其他模块。
请求 → Controller(路由绑定)→ Provider(业务逻辑)→ 响应
↑
Module 注册并管理依赖关系一个 Controller 不会自己 new 一个 Provider。它只需要在构造函数里声明类型,NestJS 的依赖注入容器会把实例注入进来。
typescript
// Controller 只声明依赖,不负责创建
@Controller('users')
class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
}UsersService 没有被显式实例化。NestJS 在启动时扫描所有模块的 providers 注册,根据类型元数据创建实例并注入到需要它的 Controller 中。Controller 不需要知道 UsersService 从哪来、怎么初始化、是否是单例——这些都由框架的 IoC 容器处理。
工作原理
NestJS 在启动时执行的过程大致分为三个阶段:
第一阶段:模块发现。 从根模块开始,递归解析 imports 声明的子模块,构建完整的模块依赖图。
第二阶段:实例化。 遍历模块图中注册的所有 Provider,分析构造函数的参数类型,按依赖顺序创建实例。如果 A 依赖 B,框架保证 B 先被创建。
第三阶段:路由绑定。 扫描所有 Controller,提取装饰器上附带的路径、方法、参数元数据,向底层 HTTP 框架(默认 Express)注册路由处理函数。
这个过程不是运行时动态执行的。NestJS 利用 TypeScript 的 emitDecoratorMetadata 编译选项,在编译阶段就把类型信息保留到了 JavaScript 输出中,框架在启动时直接读取这些元数据完成依赖解析。
基本用法
Nest CLI 是创建和管理 Nest 项目的标准工具。
bash
# 安装 CLI 并创建项目
$ npm i -g @nestjs/cli
$ nest new my-project
# 生成模块、控制器和提供者
$ nest g module users
$ nest g controller users
$ nest g service usersCLI 生成的目录结构遵循一致的约定:
src/
users/
users.module.ts # 模块声明
users.controller.ts # 控制器
users.service.ts # 服务提供者
app.module.ts # 根模块
main.ts # 入口这种约定的实际作用在于:任何一个熟悉 NestJS 的开发者打开项目,不需要翻找代码就能知道某个接口的 Controller 在哪、Service 在哪、模块依赖关系怎么声明的。结构是预定义的,决策成本为零。
装饰器是 NestJS 中另一个关键机制。除了 @Module()、@Controller()、@Injectable() 这些类型标记外,路由装饰器用于绑定请求方法和路径:
typescript
@Get('profile')
getProfile(@Param('id') id: string) { }
@Post()
create(@Body() dto: CreateUserDto) { }@Get()、@Post() 绑定 HTTP 方法和路径,@Param()、@Body() 提取请求参数。框架在启动时将这些元数据注册为对应的 Express 路由,参数提取在请求进入时自动执行。开发者不需要手动从 req 对象上取参数,也不需要关心底层是 Express 还是 Fastify——元数据抽象了这一层差异。
命令
Nest CLI 的 nest g 命令不仅能创建文件,还会自动更新模块的声明:
bash
$ nest g service users
CREATE src/users/users.service.ts
UPDATE src/users/users.module.tsUPDATE 这一行是关键——CLI 自动把新生成的 Service 添加到了模块的 providers 数组中。手动创建文件容易漏掉这一步,导致 Provider 虽然存在但不会被 NestJS 的 IoC 容器管理,注入时会直接报错。
CLI 还支持生成其他组件:
bash
$ nest g guard auth # 生成守卫
$ nest g pipe validate # 生成管道
$ nest g interceptor log # 生成拦截器示例
一个完整的模块通常由三部分组成。以用户模块为例:
typescript
// users.service.ts - 业务逻辑封装
@Injectable()
export class UsersService {
private users = [];
findAll() {
return this.users;
}
create(user) {
this.users.push(user);
return user;
}
}
// users.controller.ts - 路由和参数处理
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
list() {
return this.usersService.findAll();
}
@Post()
add(@Body() body) {
return this.usersService.create(body);
}
}
// users.module.ts - 聚合与注册
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}启动后,对 GET /users 的请求会经过 Controller 的 list 方法。UsersService 实例由框架自动注入,Controller 不需要知道如何创建 Service,也不关心它的生命周期。对 POST /users 的请求同理,@Body() 装饰器自动从请求体中提取数据传入 add 方法。
注意点
NestJS 默认使用单例作用域。同一个 Provider 实例会被注入到所有依赖它的地方。这意味着 Provider 不应该持有请求级别的状态——如果一个 Service 在方法里修改了实例属性,这个修改会影响后续所有请求。需要用请求作用域时需要显式声明 @Injectable({ scope: Scope.REQUEST }),但这会带来性能开销,因为框架需要为每个请求创建新的 Provider 实例。
框架底层默认使用 Express,切换为 Fastify 需要调用 NestFactory.create(AppModule, new FastifyAdapter())。两者在中间件兼容性上存在差异——Express 中间件生态不能直接在 Fastify 适配器下使用,需要寻找对应的 Fastify 插件。
TypeScript 的 emitDecoratorMetadata 必须开启,否则 NestJS 在运行时无法获取构造函数的参数类型,依赖注入会失效。tsconfig.json 中 target 至少需要 ES6 才能使用装饰器。
限制
NestJS 的架构约束在小型项目中可能是负担。一个只有几个接口的服务,用 Express 十几行代码就能完成,用 NestJS 需要创建 Module、Controller、Service 三个文件,还要维护 @Module() 的声明关系。
学习和理解成本也不低。装饰器、依赖注入、模块系统、TypeScript 高级类型——对于从 Express 直接转过来的开发者来说,这些概念不是 Node.js 生态的原生范式,需要适应期。
NestJS 在底层仍然依赖 Express 或 Fastify 做 HTTP 处理。如果遇到了底层框架的性能瓶颈或 Bug,排查链路会多一层。例如 Fastify 的插件 API 和 NestJS 的 Provider 模型之间不是完全对等的,某些 Fastify 特性需要通过自定义 Provider 才能暴露出来。
架构组织对比:NestJS vs Express/Koa
Express 的核心组织单元是路由和中间件。一个 Express 应用本质上是按顺序执行的中间件链,路由定义是中间件链中的一个环节。
javascript
// Express: 路由定义和业务逻辑在同一层级
const express = require('express');
const app = express();
app.get('/users', async (req, res) => {
// 业务逻辑直接嵌入路由回调
const users = await db.query('SELECT * FROM users');
res.json(users);
});这种组织方式在小项目里很自然——看到路由就知道它做什么。但规模增长后,业务逻辑、数据访问、请求校验、错误处理都挤在同一个文件甚至同一个回调里。Koa 通过 async/await 改善了异步处理,但组织逻辑的方式和 Express 没有本质区别。
NestJS 的做法是把请求处理拆成多个职责层:
- 路由绑定归 Controller
- 业务逻辑归 Provider
- 请求前置处理归 Guard、Pipe、Interceptor
- 模块边界和依赖归 Module 声明
同一段用户列表查询,在 NestJS 中会分散到不同结构的类中,每个类只做一件事。这让单元测试可以针对 Provider 单独编写,Controller 可以用 Mock 的 Provider 注入后独立测试,模块依赖关系可以从 imports 声明中直接看到。
控制反转(IoC)是这组差异的根因。Express 的程序员自己管理实例创建和依赖引用——在路由回调里 import Service 模块或直接 new 一个数据库连接。NestJS 把对象创建和依赖管理交给了框架容器,开发者只需要声明“我需要这个类型”,不用关心它从哪来、怎么初始化的。
在测试场景中,这种差异会进一步放大。要测试一个 Express 路由,通常需要启动整个 HTTP 服务或用 supertest 发起模拟请求。要测试一个 NestJS 的 Provider,直接 new 这个类并手动传入 Mock 依赖即可;Controller 可以用框架提供的 Test.createTestingModule 传入依赖替身后单独实例化,不需要启动 HTTP 服务器。
应用
NestJS 对以下场景有明确的适配优势:
微服务架构。 NestJS 除了 HTTP 传输外,内置支持 Redis、NATS、Kafka、gRPC 等消息传输层。模块化的架构让每个微服务内部保持一致的代码组织,跨服务的通信模式通过统一装饰器 API 切换。
企业级 CRUD 应用。 接口数量多、逻辑分层明确的中大型后端应用是 NestJS 最主流的使用场景。Controller→Service→Repository 的分层模式与 NestJS 的模块结构天然契合。
需要 OpenAPI 文档的 API 服务。 NestJS 的 @nestjs/swagger 包可以根据装饰器元数据自动生成 Swagger 文档,无需额外维护文档注解文件。
WebSocket 和 GraphQL 混合应用。 NestJS 把 WebSocket 网关和 GraphQL 解析器都视为 Provider 的特化形式,和 REST Controller 共用同一套模块和依赖注入系统。
适用边界
以下场景需要谨慎评估:
极简 API 或 Serverless 函数。 单个文件就能完成的服务,引入 NestJS 的模块体系是过度设计。AWS Lambda 或 Cloudflare Workers 这类冷启动敏感的环境,框架初始化开销也值得关注。
团队对 TypeScript 和装饰器的接受度有限。 NestJS 深度依赖 TypeScript 特性,如果团队主要使用纯 JavaScript 或对装饰器语法不熟悉,学习成本会被放大。
已有复杂 Express/Koa 应用想渐进迁移。 NestJS 虽然可以通过 NestFactory.create 挂载到已有 Express 实例上,但模块化重构基本等于重写,没有平稳的渐进路径。
对底层 HTTP 处理有高度定制需求。 NestJS 抽象了底层 HTTP 框架的细节,如果需要深度定制连接池、响应流、中间件执行顺序等底层行为,直接用 Express 或 Fastify 会更直接。
