用 Express / Koa 写小项目很爽,但项目一大,你会发现满屏的 require、手动 new 出来的实例、到处传递的数据库连接…… 代码逐渐变成一团乱麻。
NestJS 就是来解决这个问题的。它借鉴了 Angular 和 Spring 的思想,把依赖注入(DI)和模块化带进 Node 后端。这篇讲清它的核心三件套和 DI 到底好在哪。
三件套:Controller、Provider、Module
Nest 的世界由三种基本角色构成,理解它们就理解了 Nest 的骨架。
Controller(控制器):负责接收请求、返回响应,只做"路由 + 参数 + 调用 service",不写业务逻辑:
import { Controller, Get, Param } from '@nestjs/common'
import { PostsService } from './posts.service'
@Controller('posts')
export class PostsController {
// 构造函数里"声明"我需要 PostsService,Nest 自动塞进来
constructor(private readonly postsService: PostsService) {}
@Get(':id')
findOne(@Param('id') id: string) {
return this.postsService.findById(Number(id))
}
}Provider(提供者 / 服务):真正干活的地方,业务逻辑都在这。用 @Injectable() 标记它"可被注入":
import { Injectable } from '@nestjs/common'
@Injectable()
export class PostsService {
findById(id: number) {
// 真正的业务逻辑、数据库查询写在这里
return { id, title: '示例文章' }
}
}Module(模块):把相关的 Controller 和 Provider 组织在一起,声明依赖关系:
import { Module } from '@nestjs/common'
import { PostsController } from './posts.controller'
import { PostsService } from './posts.service'
@Module({
controllers: [PostsController],
providers: [PostsService],
exports: [PostsService], // 导出后,别的模块才能用它
})
export class PostsModule {}什么是依赖注入(DI)
注意上面 PostsController 的构造函数:
constructor(private readonly postsService: PostsService) {}我们没有 new PostsService(),只是"声明"了一句"我需要一个 PostsService"。Nest 在启动时自动帮你创建实例并"注入"进来 —— 这就是依赖注入。
对比一下传统写法的痛点:
// ❌ 传统方式:自己 new,依赖关系硬编码
class PostsController {
private postsService: PostsService
constructor() {
// 如果 PostsService 又依赖 Database、Logger...
// 你得在这里一层层手动 new,改一处动全身
const db = new Database(config)
const logger = new Logger()
this.postsService = new PostsService(db, logger)
}
}依赖一旦多起来、嵌套起来,手动 new 就是噩梦。而且测试时想换成 mock 也极其麻烦。
DI 把"创建依赖"这件事从你手里拿走,交给一个叫 **IoC 容器(控制反转容器)**的东西统一管理。你只管声明"我要什么",容器负责"给你什么"。
控制反转(IoC)是什么意思
"控制反转"听着玄,其实就一句话:对象创建和依赖装配的控制权,从你的代码反转给了框架。
- 传统方式:你的代码主动
new出依赖,控制权在你手里 - IoC 方式:你只声明需求,框架(容器)在背后创建好一切并装配给你
带来的好处是实打实的:
- 解耦:Controller 不关心
PostsService怎么被创建,换实现不用改 Controller - 单例复用:Nest 默认把 Provider 作为单例管理,整个应用共用一个实例,省内存
- 易测试:测试时用容器把真实 Provider 替换成 mock,一行配置搞定
怎么替换实现:Provider 的高级用法
DI 的威力在"替换"时才完全显现。Nest 允许你用 token 自定义提供什么:
@Module({
providers: [
// 用一个值充当 Provider
{ provide: 'CONFIG', useValue: { db: 'mysql://...' } },
// 用工厂函数动态创建
{
provide: 'DB_CONNECTION',
useFactory: async () => {
return await createConnection(/* ... */)
},
},
// 接口换实现:把 PostsService 偷偷换成另一个类
{ provide: PostsService, useClass: MockPostsService },
],
})
export class AppModule {}注入自定义 token 用 @Inject:
constructor(@Inject('CONFIG') private config: AppConfig) {}这种能力让"测试环境用内存数据库、生产环境用真 MySQL"这类需求,变成了改一行配置的事。
模块化:大型项目的组织方式
随着功能变多,Nest 鼓励你按业务域拆模块:UsersModule、PostsModule、AuthModule…… 每个模块自己管自己的 Controller 和 Service,通过 imports / exports 互相协作:
@Module({
imports: [PostsModule, UsersModule, AuthModule], // 组合子模块
})
export class AppModule {}// PostsModule 想用 UsersService,就 import UsersModule
@Module({
imports: [UsersModule], // 前提:UsersModule 里 exports 了 UsersService
controllers: [PostsController],
providers: [PostsService],
})
export class PostsModule {}这种"高内聚、低耦合"的边界,让大型项目即使有几十个模块也能保持清晰,新人也容易找到代码该写在哪。
请求生命周期:不只是中间件
Nest 在中间件之外,还提供了一套更细粒度的"切面"工具,各管一段:
| 机制 | 职责 | 典型用途 |
|---|---|---|
| Middleware | 最外层,贴近原生 | 日志、CORS |
| Guard | 路由能不能进 | 鉴权、权限 |
| Interceptor | 包裹前后,可改返回值 | 统一响应格式、缓存、耗时统计 |
| Pipe | 处理入参 | 参数校验、类型转换 |
| Exception Filter | 兜住异常 | 统一错误响应 |
这套分工比"一把梭中间件"更清晰。比如鉴权用 Guard、参数校验用 Pipe、统一返回结构用 Interceptor,各司其职。
该不该用 Nest
给个实在的判断:
- 适合用:中大型项目、多人协作、业务模块多、需要长期维护、团队熟悉 TypeScript
- 不必用:几个接口的小工具、个人脚手架、追求极致轻量。这种场景 Express / Koa 反而更省事
Nest 的代价是前期概念多、模板代码多,但换来的是大项目里的可维护性和一致性。这是一笔值不值得的权衡,取决于你的项目规模。
小结
- Nest 三件套:Controller 管路由、Provider 管业务、Module 管组织
- 依赖注入:你只声明"要什么",IoC 容器负责创建并装配,告别手动
new - 控制反转:对象创建权从你的代码交给框架,换来解耦、单例复用、易测试
- Provider 支持
useValue/useFactory/useClass,轻松替换实现做 mock - 按业务域拆 Module,靠
imports/exports协作,大项目也清晰 - Guard / Interceptor / Pipe / Filter 各管一段,比裸中间件分工更细
- 中大型、多人、长期维护的项目值得上 Nest;小工具用 Express/Koa 更轻
理解了 DI 和模块化,你会发现 Nest 不是"更复杂的 Express",而是一套帮你把大项目管理得井井有条的工程化方案。