当你第一次打开一个 NestJS 项目,看到的不是一坨
app.js,而是一堆@Module、@Controller、@Injectable装饰器时,可能会觉得"这也太绕了吧"。但一旦理解了它背后的设计模式,你会发现这种结构恰恰是大型项目保持可维护性的关键。
NestJS 是什么
NestJS 是一个用于构建 Node.js 后端服务的框架,默认使用 TypeScript。它的设计目标是企业级------当你面对的是几十个接口、多个业务模块、团队协作开发时,NestJS 的模块化思想能让代码不至于变成一团乱麻。
它底层基于 Express(也可以切到 Fastify),但在 Express 之上提供了一套完整的架构约定,核心是三件事:
| 特性 | 作用 | 对应概念 |
|---|---|---|
| 模块化 | 按业务拆分独立单元 | @Module() |
| 依赖注入 | 自动装配 Service 到 Controller | @Injectable() |
| 装饰器 | 给类动态叠加路由、校验等能力 | @Get() @Controller() 等 |
在深入这些之前,先从一个最根本的设计模式说起------工厂模式。
工厂模式:蜜雪冰城的启示
NestJS 的入口函数 NestFactory.create() 本质上就是工厂模式的运用。要理解它,先看一个更生活化的例子。
一个奶茶工厂
想象你去蜜雪冰城点单。你不需要知道冰激凌怎么做的、柠檬茶怎么泡的------你只需要跟前台说一句"来一个冰激凌",工厂内部负责生产,你拿到成品就行。
用代码表达就是这样的:
js
// 产品类:每个产品都有相同的接口(show 方法)
class IceCream {
constructor() {
this.name = '冰激凌';
this.price = 3;
}
show() {
console.log(this.name, this.price + '元');
}
}
class LemonTea {
constructor() {
this.name = '柠檬茶';
this.price = 4;
}
show() {
console.log(this.name, this.price + '元');
}
}
class MilkTea {
constructor() {
this.name = '珍珠奶茶';
this.price = 8;
}
show() {
console.log(this.name, this.price + '元');
}
}
// 工厂类:对外只暴露一个 create 方法
class MixueFactory {
static create(type) {
switch (type) {
case 'ice': return new IceCream();
case 'lemon': return new LemonTea();
case 'milk': return new MilkTea();
}
}
}
// 消费者:不需要了解工厂内部细节,直接调用
const drink1 = MixueFactory.create('ice');
drink1.show(); // 冰激凌 3元
const drink2 = MixueFactory.create('lemon');
drink2.show(); // 柠檬茶 4元
const drink3 = MixueFactory.create('milk');
drink3.show(); // 珍珠奶茶 8元
工厂模式解决了什么
| 没有 工厂 | 有工厂 |
|---|---|
| 调用者需要知道每个产品类的名字和构造方式 | 调用者只需知道工厂和一个类型参数 |
| 产品类一多,调用代码就混乱 | 新增产品只需改工厂内部,调用代码不变 |
| 产品类和调用代码强耦合 | 工厂屏蔽了内部细节,实现解耦 |
核心思想:面向接口编程,而不是面向实现编程 。所有产品都实现了 show() 方法,工厂返回哪个具体类,调用者不需要关心。
NestFactory:NestJS 的工厂
理解了蜜雪冰城,再看 NestJS 的入口就顺理成章了:
ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
// NestFactory 就是那个"工厂"
// create(AppModule) 就像 MixueFactory.create('ice')
// 你传入一个模块,工厂帮你创建出一个完整的应用实例
const app = await NestFactory.create(AppModule);
// 启动 HTTP 服务,监听 3000 端口
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
NestFactory.create(AppModule) 做的事情,和 MixueFactory.create('ice') 是同一个思路:你把需求交给工厂,工厂负责组装和返回实例。NestJS 可以构建的远不止 HTTP 服务------它还支持 WebSocket、微服务、gRPC 等,NestFactory 就是那个统一的生产入口。
装饰器模式:给类叠加能力
NestJS 的代码里到处都是 @ 开头的语法,这就是装饰器。
装饰器的本质是:不修改原有类的代码,动态给它叠加额外功能。
ts
@Controller('todos') // 给这个类贴上"控制器"标签,路由前缀是 /todos
export class TodosController {
@Get() // 给这个方法贴上"处理 GET 请求"标签
findAll() { ... }
@Post() // 给这个方法贴上"处理 POST 请求"标签
create() { ... }
}
| 传统 Express 写法 | NestJS 装饰器写法 |
|---|---|
app.get('/todos', (req, res) => { ... }) |
@Get() 贴在方法上,路由自动注册 |
| 路由和逻辑代码混在一起 | 路由声明和业务代码分离,可读性强 |
| 没有类型约束 | TypeScript 类型检查全程保驾护航 |
NestJS 把装饰器用到了极致:@Module() 定义模块、@Controller() 定义控制器、@Injectable() 标记可注入服务、@Get() @Post() @Delete() 定义路由方法、@Param() @Body() 提取请求参数。每个装饰器各司其职,把一个普通类变成了功能完整的后端组件。
注意:TypeScript 默认不开启装饰器支持。NestJS 项目的
tsconfig.json中必须配置"experimentalDecorators": true和"emitDecoratorMetadata": true,这也是nest new脚手架自动帮你做好的。
模块化架构:Module → Controller → Service
NestJS 的核心架构可以用一句话概括:
ini
AppModule(根模块)
├── imports: [其他子模块...]
├── controllers: [控制器列表]
└── providers: [服务列表]
三层职责划分
| 层 | 文件约定 | 职责 | 装饰器 |
|---|---|---|---|
| Module | xx.module.ts |
组装模块,声明依赖关系 | @Module() |
| Controller | xx.controller.ts |
接收请求、参数校验、返回响应 | @Controller() |
| Service | xx.service.ts |
业务逻辑、数据处理 | @Injectable() |
这就是经典 MVC 思想在 NestJS 中的映射:
- M(Model/Service) :数据服务和业务逻辑层,通过
@Injectable()标记,可被自动注入 - C(Controller):控制器层,处理 HTTP 请求的入口,校验参数后把业务交给 Service
- V(View):NestJS 是纯后端框架,视图层由前端负责
根模块长什么样
ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodosModule } from './todos/todos.module';
@Module({
imports: [TodosModule], // 依赖的子模块
controllers: [AppController], // 本模块的控制器
providers: [AppService], // 本模块的服务
})
export class AppModule {}
@Module() 装饰器接收一个配置对象,三个关键字段:
imports:声明本模块依赖哪些其他模块(比如TodosModule)controllers:注册本模块的控制器providers:注册本模块的服务(Service)
依赖注入:Service 自动到 Controller
这是 NestJS 最"魔法"的地方。在 Controller 中:
ts
@Controller()
export class AppController {
// 没有手动 new AppService(),直接声明就拿到了
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
你没有写 new AppService(),但 this.appService 就能用了。这是因为:
AppService用@Injectable()标记为可注入AppModule的providers中注册了AppService- NestJS 的 IoC 容器在创建
AppController时,自动实例化AppService并注入到构造函数中
| 手动实例化 | 依赖注入 |
|---|---|
const service = new AppService() 每处都要写 |
容器自动创建并注入 |
| Service 换实现类,所有调用处都要改 | 只改 Module 注册,调用处不变 |
| 无法轻松 mock 做单元测试 | 测试时可替换为 mock 实现 |
项目目录结构一览
一个标准的 NestJS 项目结构如下:
ruby
hello/
├── src/
│ ├── main.ts # 入口文件:创建应用、启动服务
│ ├── app.module.ts # 根模块:组装所有模块
│ ├── app.controller.ts # 根控制器:处理 / 路由
│ ├── app.service.ts # 根服务:返回 Hello World
│ └── todos/ # 业务模块(独立目录)
│ ├── todos.module.ts # 模块定义
│ ├── todos.controller.ts # 控制器:RESTful 路由
│ └── todos.service.ts # 服务:CRUD 业务逻辑
├── test/
│ └── app.e2e-spec.ts # E2E 测试
├── package.json
├── tsconfig.json # TypeScript 配置(开启装饰器)
└── nest-cli.json # NestJS CLI 配置
关键约定:
- 入口 :
main.ts负责引导启动,调用NestFactory.create(AppModule)创建应用 - 模块 :每个业务功能是一个独立目录,包含
module.ts、controller.ts、service.ts三件套 - 组装 :根模块
app.module.ts通过imports引入所有子模块,形成依赖树
从零启动一个 NestJS 项目
bash
# 全局安装 NestJS CLI
npm i -g @nestjs/cli
# 创建新项目
nest new hello
# 进入项目目录
cd hello
# 开发模式运行(文件变更自动重启)
npm run start:dev
打开浏览器访问 http://localhost:3000,看到 Hello World! 就说明项目已经跑起来了。
package.json 中几个关键命令:
| 命令 | 作用 |
|---|---|
npm run start |
普通启动 |
npm run start:dev |
开发模式,热重载 |
npm run build |
编译到 dist/ 目录 |
npm run start:prod |
生产模式,运行编译后的 dist/main.js |
npm test |
运行单元测试 |
npm run test:e2e |
运行端到端测试 |
小结
NestJS 的架构设计并不复杂,核心就是三个设计模式的组合运用:
- 工厂模式 :
NestFactory.create()统一创建应用实例,屏蔽内部组装细节 - 装饰器模式 :
@Module@Controller@Injectable等装饰器给类叠加能力,不改原有代码 - 依赖注入:Service 不需要手动实例化,由 IoC 容器自动注入到需要的地方
理解了这套架构之后,再去看任何 NestJS 项目,你会发现它们都遵循同一个模式:入口创建应用 → 模块组织结构 → 控制器处理请求 → 服务处理业务。掌握了这个套路,就等于拿到了 NestJS 的万能钥匙。
下一篇,我们会用一个完整的 Todos CRUD 模块,把这套架构从纸面概念变成可运行的代码。