NestJS 最小项目全景拆解:NestFactory 工厂、@装饰器、Module 模块
你在浏览器打开 localhost:3000,看到一行 Hello World!。
这一行字,是从哪个文件流出来的?你翻开源码,整个项目就 4 个源文件,每个都短得不像话:
text
src/
├── main.ts # 入口
├── app.module.ts # 根模块
├── app.controller.ts # 控制器
└── app.service.ts # 服务
4 个文件,加起来不到 50 行代码。但中间发生了什么、为什么这么拆,网上的教程大多只教你「照抄」,很少一次讲清。
这篇文章把 NestJS 最小项目的骨架彻底拆开,重点不是背 API,而是看懂它背后贯穿始终的三种设计模式,以及「为什么这样设计」。 看完你会知道:NestJS 不是另一个 Express 的写法,它是用「声明式装配」的思路,把后端从脚手架时代带进了框架时代。
先看全景:一行 Hello World 走了四层
text
浏览器 GET localhost:3000
│
▼
① main.ts NestFactory.create(AppModule) [工厂模式:装配整个应用]
│
▼
② app.module.ts @Module({ controllers, providers }) [模块化:声明边界]
│ 依赖注入
▼
③ app.controller.ts @Controller / @Get() [装饰器:注册路由]
│ 调用 this.appService.getHello()
▼
④ app.service.ts @Injectable() 返回 'Hello World!' [服务:业务逻辑]
│
▼
浏览器收到 "Hello World!"
记住这张图,下面每一层都对应一次「为什么要这么设计」。
第一层 main.ts:NestFactory 就是「蜜雪冰城」
整个应用从这一行开始:
typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule); // 🔑 工厂方法:一个调用,装配整个应用
await app.listen(process.env.PORT ?? 3000); // 启动 HTTP 服务,默认 3000
}
bootstrap();
NestFactory.create(AppModule) 这个调用,背后做的事远不止 new 一个对象:它要创建依赖注入容器、扫描模块、注册路由、装配中间件/管道/守卫/拦截器、初始化生命周期钩子......
这一整套复杂装配,被一个 create() 封装起来了。 这就是工厂模式------你不需要知道「一辆车怎么从零件组装起来」,你只需要跟「4S 店」打交道,说一句「我要一辆车」。
我拿蜜雪冰城打个比方(这是我理解工厂模式的钥匙):
javascript
// 工厂类:你只跟它打交道,不用关心内部有多少种产品、怎么做的
class MixueFactory {
static create(type) {
switch (type) {
case 'ice': return new IceCream(); // 冰激凌 3 元
case 'lemon': return new LemonTea(); // 柠檬水 4 元
case 'milk': return new MilkTea(); // 珍珠奶茶 8 元
}
}
}
const drink = MixueFactory.create('lemon'); // 你只要「点单」,不关心泡茶加冰封杯
drink.show();
NestFactory 就是那个蜜雪冰城工厂,AppModule 就是你要的「产品」。工厂模式解决的是「创建过程太复杂、调用方不想关心细节」的问题。
第二层 app.module.ts:模块化 = 声明「我提供什么、我需要什么」
工厂造出来的产品,是一整个 AppModule:
typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [], // 🔑 我需要依赖的其它模块
controllers: [AppController], // 🔑 我提供的「入口层」(路由、参数校验)
providers: [AppService], // 🔑 我提供的「服务层」(业务逻辑、CRUD)
})
export class AppModule {}
一个 @Module 就声明了三件事:我依赖谁(imports)、我暴露哪些控制器(controllers)、我提供哪些服务(providers)。
为什么要把后端拆成模块?因为一个真实的后端动辄几百个类、几十张表,如果全塞在一个文件里,谁也维护不动。模块化就是把「一个 feature」圈成一个边界------用户模块、订单模块、支付模块,各自声明自己的 imports / controllers / providers,互不越界,只通过 imports 显式依赖。
这就是 NestJS 和 Express 最大的分水岭:Express 给你自由(想怎么写都行),NestJS 给你结构(按模块、控制器、服务三件套组织)。
第三层 app.controller.ts:@装饰器 = 给类贴标签
模块声明了控制器,那控制器长什么样?
typescript
// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller() // 🔑 告诉 Nest:「这是一个控制器,负责接 HTTP 请求」
export class AppController {
constructor(private readonly appService: AppService) {}
@Get() // 🔑 告诉 Nest:「把这个方法注册为 GET / 路由」
getHello(): string {
return this.appService.getHello(); // 具体返回什么,交给 service
}
}
注意 @Controller、@Get 这两个 @ 开头的语法------这就是装饰器模式。
装饰器模式的本质是:不修改类本身,只给它「贴标签」,让框架根据标签自动做额外的事。 你写了 @Get(),不需要写任何「注册路由」的配置代码,Nest 就自动把这个方法挂到了 GET / 上。
那 Nest 是怎么「看见」这些标签的?靠的是 reflect-metadata------装饰器在类上写入元数据,Nest 启动时扫描这些元数据,决定「这是控制器」「这是 GET 路由」「这个 service 该被注入」。这就是「声明式」:你只声明意图,框架负责落地。
对比一下「命令式」的 Express,你会立刻懂装饰器省了多少事:
javascript
// ❌ Express:路由要你自己显式写
app.get('/', (req, res) => res.send('Hello World!'));
typescript
// ✅ Nest:@Get 一个注解,路由自动注册
@Get()
getHello(): string { return this.appService.getHello(); }
第四层 app.service.ts:@Injectable 补上依赖注入的最后一环
控制器没自己算「Hello World」,而是调了 this.appService.getHello()。这个 appService 从哪来的?它没被 new 过。
typescript
// src/app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable() // 🔑 标记「这个类可以被注入到别人那里」
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
@Injectable() 把这个类登记进 Nest 的依赖注入(DI)容器。控制器在构造函数里写 constructor(private readonly appService: AppService),Nest 就会自动「看类型,把对应的实例塞进来」------你声明「我要一个 AppService」,框架负责「怎么给」。
这就是依赖注入 的价值:控制器不负责创建服务,只负责声明依赖。好处是解耦 + 可测试------你可以随时把 AppService 换成 mock,控制器一行都不用改。
测试文件里,藏着 DI 的真正好处
翻到 src/app.controller.spec.ts,你会看到 DI 最实在的回报:
typescript
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController], // 🔑 手动装配:想测哪个,就把哪个装进去
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
因为控制器不自己 new 服务,所以测试时可以手动指定装配什么 。要测控制器,就装 controller + 一个假 service;要单独测 service,就只装 service。如果当初控制器内部自己 new AppService(),这种「替换」就做不到了。
e2e 测试 test/app.e2e-spec.ts 更是把整个 AppModule 装起来,用 supertest 真实发一个 GET / 请求,断言返回 Hello World!------等于把上面四层链路完整跑了一遍。
三种模式,其实是一件事
拆开看是工厂、装饰器、模块化三个名词,串起来你会发现它们是同一个思想:
NestJS 让你「只声明要什么,不写怎么装配」。
- 工厂 (
NestFactory.create):把「怎么装配整个应用」藏起来 - 装饰器 (
@Module/@Controller/@Get/@Injectable):把「我是谁、我要挂在哪」声明出来 - 模块 + 依赖注入 (
imports/providers):把「谁依赖谁」声明出来,框架自动注入
从 nest new 出来的 4 个文件,就是这套思想的最小演示:你负责声明结构,框架负责把它跑起来。
结尾:记住「声明式装配」这四个字
回到开头那个问题------localhost:3000 的那行 Hello World,到底走了几层。
它走了四层:main.ts 用工厂造出应用 → app.module.ts 声明模块边界 → app.controller.ts 用装饰器挂上路由 → app.service.ts 被依赖注入、返回数据。
如果只记住一句话,记住这句:
NestJS 的核心不是某一个 API,而是「声明式装配」------你写
@声明意图、写imports/providers声明依赖,剩下复杂的创建、注入、路由注册,全部交给框架。
下次再看 NestJS 代码,别纠结某个装饰器的写法了。先问自己一句:「这段代码在声明什么?它依赖谁、又被谁依赖?」 顺着 imports → controllers → providers 这条线摸下去,再大的后端项目也能一眼看懂骨架。
留一个开放问题:模块化的反面是「把所有东西写在一个文件里」。你觉得一个后端项目,什么时候该拆模块、什么时候拆得太碎反而过度设计?欢迎在评论区聊聊你的判断标准。