系列第 01 篇 · 从一个 hello 项目出发
零、这篇文章讲什么
我第一次接触 NestJS,花了一个周末把官方 nest new 生成的 hello 项目从头到尾过了一遍。这一篇是我对整个框架的第一版理解:NestJS 到底靠什么把后端代码组织得井井有条的?
阅读这篇文章你会得到三样东西:
- 一张 NestJS 的知识架构图(也是整个博客系列的地图)
- 四个核心概念(模块化 / 工厂 / 装饰器 / MVC)的直观理解
- 一个贯穿全篇的"请求旅程",把四个概念串成一条线
前置知识:会一点 TypeScript 的 class 和装饰器语法就够。不需要后端经验。
一、为什么需要 NestJS
先看痛点。用 Express 这类轻量框架写后端,一开始很爽:
js
app.get('/todos', (req, res) => { /* 查列表 */ });
app.get('/todos/:id', (req, res) => { /* 查详情 */ });
app.post('/todos', (req, res) => { /* 新增 */ });
但项目一变大就失控:路由、参数校验、数据库访问、鉴权、日志全堆在 app.js 里。Express 给你自由,但不给你结构------代码怎么写全凭个人风格,换个人就换一套写法。
NestJS 解决的就是这个问题。它的定位可以一句话说清楚:
NestJS 是 Node.js 世界的 Spring Boot。默认 TypeScript、强制分层、内置依赖注入,把"企业级后端该有的结构"用约定固化下来。
| 特性 | Express | NestJS |
|---|---|---|
| 语言 | JS 为主 | 默认 TS,强类型 |
| 代码组织 | 自由,想怎么写怎么写 | 强制 Controller / Service / Module 分层 |
| 依赖注入 | 没有 | 内置 DI 容器 |
| 团队协作 | 一人一种风格 | 约定统一,守住代码质量下限 |
NestJS 不是另一个"写接口更快的库",而是一套后端工程的纪律。本文要讲的四个核心概念,就是这套纪律的四根柱子。
二、知识架构图
这是我理解 NestJS 的第一版地图,也是本博客系列的总纲:
java
┌─────────────────────────────────────────────────┐
│ main.ts 入口 │
│ const app = await NestFactory.create(...) │
│ 【工厂模式:组装应用】 │
└───────────────────────┬─────────────────────────┘
│ 读取 @Module 元数据
┌───────────────────────▼─────────────────────────┐
│ AppModule 根模块 │
│ 【模块化:应用是一棵树】 │
│ imports / controllers / providers │
└───────────────────────┬─────────────────────────┘
│ 递归展开整棵模块树
┌─────────────────────────────────┼─────────────────────────────────┐
│ │ │
┌──▼───────────────┐ ┌───────▼──────────┐ ┌─────────▼─────────┐
│ Controller 层 │ │ Provider 层 │ │ 子模块 │
│ MVC 的 C │ │ MVC 的业务层 │ │ TodosModule │
│ AppController │ ──────► │ AppService │ │ (imports 引入) │
│ 接请求 / 回响应 │ 依赖注入│ 数据与业务 │ │ │
└──────────────────┘ └──────────────────┘ └───────────────────┘
▲ ▲
│ @Controller() @Get() │ @Injectable()
└────────【装饰器模式:贴标签】──────┘
读图顺序(也是本文结构):
- 入口 →
NestFactory.create(),一个工厂方法,接收根模块 - 模块化 → 工厂读
AppModule上的元数据,递归展开出整棵模块树 - 装饰器 → Controller / Provider / Module 全靠装饰器"贴标签"声明自己是谁
- MVC + DI → 请求进来走 Controller → 注入 Provider → 返回响应
下面一节一节拆开讲。
三、核心概念巡礼
1. 模块化:应用是一棵树
NestJS 的最小组织单元是 Module(模块) 。我的 hello 项目里 app.module.ts 是这样的:
ts
@Module({
imports: [TodosModule], // 引入别的模块(子树)
controllers: [AppController], // 当前模块的控制器
providers: [AppService], // 当前模块的 provider
})
export class AppModule {}
模块化的本质:把"一群接口互相乱调"变成"每个业务一块自留地"。 三个要点:
- 模块是一棵树,不是一堆 。
AppModule是树根,imports声明子树。Nest 启动时从根递归展开,把整棵树的 controller、service 全找出来。谁依赖谁,全写在imports里,明明白白。 - 模块是作用域边界 。一个模块里的 provider 默认只在模块内可见;想给别人用,得主动
exports出去,别人imports进来。没有"全局魔法变量"。 - 模块可以复用 。一个
TodosModule可以同时被多个模块引入------这正是 Todo 这类业务模块应该有的样子。
类比:模块就像蜜雪冰城的菜单分区------甜品区、饮品区各自成块。顾客(其他模块)通过菜单(imports)点单,而不是自己翻进后厨拿货。
2. 工厂模式:把组装交给工厂
NestJS 的入口 main.ts 只有三行核心代码:
ts
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
NestFactory 就是一个工厂类 。工厂模式解决的问题是:调用方不知道"对象怎么被创建、怎么被管理",只跟工厂打交道。
我自己写过一个最小版本 factory_demo/1.mjs 来理解它:
js
class MixueFactory {
static create(type) {
switch (type) {
case "ice": return new IceCream();
case "lemon": return new LemonTea();
case "milk": return new MilkTea();
}
}
}
const drink = MixueFactory.create("ice");
drink.show(); // 冰激凌3元
对照表:
| 我的 demo | NestJS | 对应关系 |
|---|---|---|
IceCream / LemonTea |
AppController / AppService |
各种类 |
统一的 show() 接口 |
统一的构造/注入约定 | 接口约定 |
MixueFactory.create(type) |
NestFactory.create(AppModule) |
工厂方法 |
| 调用方不关心怎么 new | 你从不写 new AppService() |
解耦 |
NestFactory.create() 只是最外层的第一层工厂。它内部还藏着一层更重要的工厂------依赖注入容器,负责创建和管理所有 provider。这层会在系列第 02 篇单独展开。
3. 装饰器模式:给代码贴标签
你第一次看 NestJS 代码时一定会注意到满屏的 @:
ts
@Controller() // 类装饰器:这是一个控制器
export class AppController {
@Get() // 方法装饰器:这是一个 GET 路由
getHello(): string {
return this.appService.getHello();
}
}
装饰器模式在这里的本质是:把"这个类/方法是什么、该怎么处理"的信息,贴在代码本身旁边,而不是写进一张独立的 if-else 路由表里。
对比两种写法:
js
// 不用装饰器(Express 式):路由和处理逻辑分开写
app.get('/todos/:id', (req, res) => { /* 处理 */ });
ts
// 用装饰器(NestJS 式):路由就在方法正上方,代码即文档
@Get(':id')
findOne(@Param('id') id: string) { /* 处理 */ }
实现原理:@Controller、@Get 这些装饰器本质是函数,它们把元数据写进类的反射元数据里。工厂启动时读这些元数据,自动生成路由表。所以 main.ts 里那句注释很准确------装饰器只是描述这个类长什么样子,真正的装配发生在工厂里。
NestJS 把装饰器用到了极致,是一整个家族:
| 类型 | 装饰器 | 作用 |
|---|---|---|
| 类装饰器 | @Module @Controller @Injectable |
声明类属于哪一类 |
| 方法装饰器 | @Get @Post @Put @Delete |
声明 HTTP 路由 |
| 参数装饰器 | @Param @Body @Query @Req |
声明从哪取参数 |
| 能力装饰器 | @UseGuards @UsePipes @UseInterceptors |
挂守卫 / 校验 / 拦截器 |
4. MVC 架构:职责分层
MVC(Model-View-Controller)在 NestJS 里的映射:
javascript
Controller ──> Service ──> (Model / 数据库)
│ │
└────────────┴──> 返回 JSON 给前端(API 后端的"视图")
| MVC 概念 | NestJS 中的位置 | hello 项目里的例子 |
|---|---|---|
| C 接请求、控制流程 | *.controller.ts |
AppController.getHello() |
| M 业务逻辑 + 数据 | *.service.ts + 数据实体 |
AppService.getHello() |
| V 展示层 | 返回的 JSON(对 API 后端就是响应体) | return 'Hello World!' |
看 hello 项目里的分工 app.controller.ts 和 app.service.ts:
ts
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello(); // 控制器不写业务,交给 service
}
}
ts
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!'; // 业务层只处理数据,不认识 HTTP
}
}
MVC 的核心价值:每一层只能干自己的事。 Controller 只做"翻译"(HTTP 请求 ↔ 方法调用),Service 只处理业务数据、完全不认识 HTTP。这样分层的直接红利是测试容易 :测 Service 不用起服务器,new AppService() 直接调就行。
注意:NestJS 的 View 层通常不是服务端渲染的页面,而是返回 JSON 给前端 SPA。所以它更像一个"去掉了视图模板的 MVC"------Controller + Service(业务层)+ 数据模型。
四、串起来:一次请求的完整旅程
用 hello 项目走一遍全流程,四个概念就全通了:
java
1. 启动:NestFactory.create(AppModule)
└─ 工厂按 @Module 元数据展开模块树
└─ 读到 AppController 上有 @Get() → 生成路由表:GET / → getHello
└─ 读到构造器需要 AppService → 容器 new AppService() 并注入
2. 请求:浏览器访问 http://localhost:3000/
└─ Nest 查路由表 → 命中 AppController.getHello()
└─ getHello() 调用 this.appService.getHello() ← 注入好的实例
└─ AppService 返回 'Hello World!' ← 业务层干活
└─ Controller 把结果作为响应体返回 ← 控制层收尾
六步里四个概念全部上场:模块化提供容器结构,工厂负责装配,装饰器生成路由表,MVC 决定每层职责。 这就是 NestJS 的设计------它不是一个新语言,而是把这套被验证过的企业级模式搬到了 Node.js 上。
五、NestJS 的生态:入门之后还有这些
知道框架能做什么,才知道现在学的每个概念未来长在哪。入门之外,NestJS 内置了大量开箱即用的企业级能力(都是装饰器 + 注入的延伸):
- 管道 Pipe :参数校验(配合
class-validator),非法输入直接挡在路由外 - 守卫 Guard:权限/登录拦截,挂在路由上
- 异常层 :内置
NotFoundException等一整套标准异常类,统一 JSON 输出 - 数据库 :
@nestjs/typeorm/@nestjs/mongoose,ORM 也变成可注入的 provider - 微服务:同一套 Service 可同时暴露 HTTP / TCP / gRPC / MQ
- 测试 :
*.spec.ts,DI 让单元测试非常自然
六、学习路线图(系列地图)
我把 NestJS 的学习拆成一条主线,文章会随项目进度一篇篇补上:
kotlin
NestJS 学习主线(banckend/nestjs/)
├── ✅ 01 NestJS 是什么:一张图看懂企业级后端的骨架 ← 本篇
├── ✅ 02 依赖注入:从"你要什么"到"容器给你"
├── ⬜ 03 路由与参数:@Get @Body @Param @Query (hello/src/todos 已有素材)
├── ⬜ 04 管道与校验:class-validator 防御非法输入
├── ⬜ 05 数据库:TypeORM 持久化
├── ⬜ 06 守卫与鉴权:保护你的接口
└── ⬜ 07 模块作用域:@Global 与 provider 生命周期
每篇配套的练习代码都在 hello/(或新的子项目)里,写完一篇更新一次本文的路线图。
小结
四个概念一句话记忆:
| 概念 | 一句话 | 代码证据 |
|---|---|---|
| 模块化 | 把后端按业务切成树状的可复用模块 | imports: [TodosModule] |
| 工厂模式 | 你不 new 对象,工厂帮你 new 并管理 | NestFactory.create(AppModule) |
| 装饰器模式 | 给类/方法贴标签,启动时读标签装配 | @Controller @Get @Module |
| MVC 架构 | Controller 接请求、Service 干业务、JSON 当视图 | app.controller.ts → app.service.ts |