3 种设计模式 + DI:Nest.js 后端第一课
前面十几节课,我基本都在写前端:组件、路由、Canvas、ESLint、Next.js 全栈。代码越写越多,但每次和后端打交道还是发虚------接口怎么组织?业务逻辑放哪一层?为什么后端框架一上来就满屏 @ 装饰器?
这节课第一次正式往后端走一步。工具是 Nest.js。
笔记翻完一遍,我发现这节课表面是「学一个后端框架」,实际是用 Nest.js 当样本,把三种设计模式和依赖注入一次讲清楚------工厂模式、MVC、装饰器、DI,外加一个 Restful 思想。Nest.js 只是这些思想的载体。
一、Nest.js 是什么:先把定位说清楚
Nest.js 的定位是:
Next.js是全栈开发框架,Nest.js则是 Node.js 生态里的纯后端企业级开发框架。
它默认使用 TypeScript,强调全面的模块化开发思想,适合构建企业级服务。
后端开发做的不只是写几个接口。我把后端工作分了几类:
- 提供 API 接口,做 Web 开发
- 系统集成、并发、底层服务、AI Infra
- 微服务
这也是我理解 Nest.js 的起点:它不是为了让我少写几行代码,而是为了让我在代码越来越多的时候,仍然知道每一部分应该放在哪里。
企业级框架:不是单文件脚本能跑就行,而是要考虑代码分层、模块边界、依赖管理、错误处理、可测试性和团队协作。Nest.js 把这些都做成约定。
二、安装与第一个项目
安装流程很直接。全局安装 Nest CLI:
bash
npm i -g @nestjs/cli
初始化项目:
bash
nest new hello
启动项目:
bash
nest start
生成的项目入口主要在 src 目录:
text
src
├── main.ts # 入口文件
└── app.module.ts # 根模块
Nest.js 还在 package.json 里默认带好了一系列脚本:build、start、start:dev(watch 模式)、start:debug、start:prod(node dist/main)、lint、test、test:e2e、test:cov,以及对应的依赖。
关键依赖有几个值得记一下:
@nestjs/common:Nest 的核心常用装饰器和工具,@Module、@Controller、@Injectable、NotFoundException都从这里来@nestjs/core:Nest 的运行时核心,NestFactory就在这里@nestjs/platform-express:把 Nest 接到 Express 上的适配层reflect-metadata:装饰器元数据反射库,Nest 依赖注入能工作靠的就是它rxjs:响应式编程库,Nest 内部很多异步流程用它
reflect-metadata :TypeScript 装饰器在类、方法、属性上加的「元信息」,需要靠这个库在运行时存取。Nest 的
@Injectable()、@Controller()这些装饰器之所以能让框架「认出」这个类是干什么的,底层就是 reflect-metadata 在记录这些元数据。
三、Nest.js 的设计模式基因
笔记里列了 Nest.js 的几个关键词:工厂模式、MVC 模式、高度模块化、装饰器模式。再加上课堂上反复强调的「依赖注入」,以及最后落到 Todos 接口时的 Restful 思想。
Nest.js 不是随便挑了几个流行词堆在一起。它把这些设计模式当成骨架,整个框架的 API 都是围绕它们展开的。下面我按笔记顺序,一个一个拆。
代码书写约定大致是这样的:
text
APP(由多个模块组成)
└── Modules
└── @nestjs/common 的 Module 类
├── import # 依赖项
├── controller # 控制器:参数校验、简单逻辑、return response
└── service # 服务层:return 数据
这个骨架里就藏了三种设计模式:模块本身是 MVC 的载体,@Module 是装饰器,整个 APP 的创建走工厂。
四、设计模式之一:工厂模式
先用一个生活例子记它
设计模式是对面向对象设计经验的抽象总结,是解决特定问题的可复用方案。GoF 在《设计模式》一书中提出了 23 种经典设计模式。工厂模式是第一种也是最为重要的设计模式。
我用笔记里的类比记住它:想喝奶茶,不用自己做(不用自己写一堆流程代码),直接找对应的奶茶生产商------蜜雪冰城。
为什么蜜雪冰城是工厂?因为它有多个商品可以供我选择。我作为调用方,不需要了解每一种商品的制作细节,只需要告诉工厂自己想要哪一种。
NestFactory 在 Nest.js 里就是蜜雪冰城。它满足做不同种类 APP 的需要------REST API、WebSocket、微服务,都从同一个工厂出来。
工厂模式代码:MixueFactory
factory_demo/1.mjs 的完整代码:
javascript
// 蜜雪冰城的某一种产品
// 企业,有很多的产品,每一种产品都实现了相同的接口(方法)
// 一个企业有很多产品,开发者难以记住,甚至还有很多的企业
// 利用工厂模式,不需要了解工厂中类的实现细节
// 只需要直接与工厂交互,工厂会根据配置创建不同的产品
class IceCream {
constructor() {
console.log('IceCream constructor');
this.name = "冰激凌";
this.price = 3;
}
show(){
console.log(`${this.name} ${this.price}元`);
}
}
class LemonTea {
constructor(){
console.log('LemonTea constructor');
this.name = "柠檬茶";
this.price = 4;
}
show(){
console.log(`${this.name} ${this.price}元`);
}
}
class MilkTea {
constructor(){
console.log('MilkTea constructor');
this.name = "珍珠奶茶";
this.price = 8;
}
show(){
console.log(`${this.name} ${this.price}元`);
}
}
// 工厂类 不负责实例的创建,只负责根据配置创建不同的产品(调用对应的构造函数返回对应实例)
class MixueFactory {
static createMixue(type){
switch(type){
case 'iceCream':
return new IceCream();
case 'lemonTea':
return new LemonTea();
case 'milkTea':
return new MilkTea();
default:
throw new Error('未知的混雪类型');
}
}
}
// MixueFactory.createMixue('iceCream') 初始化并返回 冰激凌 这个类
MixueFactory.createMixue('iceCream').show();
这套代码里我注意到几件事:
- 三个产品类
IceCream、LemonTea、MilkTea都实现了相同的show()接口。这是工厂模式能成立的前提------产品必须面向同一个抽象类型(接口/抽象类),让客户端能依赖"抽象产品"而非具体产品类。这正是依赖倒置:客户端面向抽象编程,工厂负责产出具体实例。 MixueFactory是个静态方法工厂,调用方只需要MixueFactory.createMixue('iceCream'),根本不用知道IceCream的构造细节。switch里有一个default分支,遇到未知类型会throw new Error('未知的混雪类型')。工厂不是「传什么都返回一个东西」,而是明确告诉调用方:这个配置生产不了。- 这里有个容易混淆的点:不负责实例创建的是调用方,不是工厂 。调用方不直接
new具体产品,而是把创建交给工厂;工厂内部该new还是new,只是这个new被封装在工厂里,调用方看不见也不需要关心。
Nest.js 里的 NestFactory
NestFactory 就是这个模式在 Nest.js 里的实现。看 main.ts:
typescript
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
调用方只传 AppModule,不关心应用怎么被构造。工厂模式的解耦价值在于:产品接口不变,换的是具体实现------比如日志从 console 换成文件、DB 驱动从 MySQL 换成 PostgreSQL,工厂内部 new 的具体类变了,但返回的抽象类型不变,调用方这边一行都不用改。
工厂模式(Factory Pattern):创建型设计模式之一,笼统指把对象创建封装进工厂、让调用方与具体类的构造解耦。其下细分为简单工厂(传参选产品)、工厂方法(子类化工厂)、抽象工厂(产一族产品),三者形态不同但内核一致。
五、设计模式之二:MVC 模式
MVC 是什么
MVC(Model-View-Controller):把应用拆成三层------Model 负责数据模型和业务逻辑(含数据访问),View 负责展示数据(Web 场景下是 HTML 视图层),Controller 负责接收用户输入、调用 Model 取数据、返回响应(Web MVC 里不直接渲染 View,由框架的视图解析器渲染)。
如果不分层,一份服务就只有一份文件,可能几千行代码。 MVC 三层的职责:
- Model:数据模型 + 业务逻辑(含数据访问)
- View:负责展示数据,Web 场景下通常是 HTML 模板
- Controller:接收用户输入,调用 Model 取数据并返回响应
但纯后端 API 项目里没有传统意义的 View------返回的是 JSON。NestJS 实际采用的是分层架构(Controller → Service → Repository),可以看作 MVC 的演化:业务逻辑从 Model 里抽出来放进了 Service,Model 退化成 Entity(纯数据结构),View 这一层被 JSON 序列化取代。这套分层在 NestJS 里落到 Module 模块结构上。
从 main.ts 看 MVC 怎么跑
main.ts 完整代码,注释全部保留:
typescript
// nest.js 按需加载 大型框架性能优化,模块化的思考
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
// 实例化后端 nest.js 应用,app
// 面向对象思想
// nest 后端形态多变(REST / WebSocket / 微服务 ...),直接 new 会让调用方与具体实现耦合
// → 用 工厂模式 把"创建哪个应用 / 怎么创建"封装起来,调用方只需传配置(AppModule)
// / 首页路由,由 AppModule 来服务
// Module 是一个整体,是后端最常见的 MVC 模式
// Model View Controller
// 模型(Model):数据模型,负责数据的存储和检索 数据库抽象
// 视图(View):负责展示数据,通常是一个 HTML 文件 视图层 html
// 控制器(Controller):负责处理用户输入,调用模型和视图,返回响应 控制器
// 如果不启用 MVC 模式,一份服务就只有一份文件,可能几千行代码
// localhost:3000/ / 后端路由 -> 送到 AppModule 来处理 -> 组织控制器 controller(处理前端用户输入)
// -> 调用服务 service层 来处理业务逻辑(CRUD 数据库操作 SQL 代码)
const app = await NestFactory.create(AppModule);
// 启动 web http 服务,默认端口 3000
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
注释里藏着两个关键认知:
NestFactory.create(AppModule)不是普通的new。Nest 后端形态多变(REST / WebSocket / 微服务),直接new会让调用方和具体实现耦合。工厂把「创建哪个应用 / 怎么创建」封装起来,调用方只传配置(AppModule)。- 请求进入
localhost:3000/后的流向是:路由送到AppModule→ 组织controller(处理前端用户输入)→ 调用service层处理业务逻辑(CRUD / 数据库操作)→ 返回响应。
MVC 在 Nest.js 里的四层落地
大型后端框架的 MVC 架构进一步拆成四层:
text
视图层(不能直接去数据库中查询数据)
↓
控制器层
↓
服务层
↓
数据库操作层
对应到 Nest.js 的文件结构,一个业务模块通常包含三类文件:
xx.module.ts:定义组装这个模块xx.controller.ts:控制器,参数校验、简单逻辑xx.service.ts:作为 provider,向外提供数据服务,处理复杂业务、调用数据库操作层
AppModule 是根模块,再通过 imports 把这些业务模块接进来。
Module(模块) :Nest.js 里组织业务的基本单位。一个 Module 把它自己的 controller、service、依赖项封装成一个整体,根模块再通过
imports把它们组装成完整应用。
六、设计模式之三:装饰器模式
装饰器模式(Decorator Pattern) :在不直接修改原有对象或类的前提下,动态地给它叠加额外功能或描述信息。Nest.js 把这个模式用到了极致------
@符号到处都是。
装饰器在 Nest.js 里基本就是「元信息标签 + 框架注册器」。@ 装饰一个 class,既给它打上元信息(比如路由前缀、角色,描述这个类是什么、做什么),也触发 Nest 的扫描注册------框架启动时读这些元信息,把类装配进容器、绑定路由/守卫/中间件,这个类才真正"生效"。
@Module:声明一个模块
看 app.module.ts:
typescript
import { Module } from '@nestjs/common';
// 控制器 检测前端用户输入,一些控制逻辑,校验
import { AppController } from './app.controller';
// 数据库业务,一些复杂业务 CRUD 操作,调用数据库操作层,服务层
import { AppService } from './app.service';
import { TodosModule } from './todos/todos.module';
// 复杂,有一份说明书,照着做
// 声明一个模块,() 中的是 配置项
// 装饰器模式
// 快速的给 类 添加一些行为和方法
// 装饰器模式需要 ts + es6 支持
@Module({
imports: [TodosModule], // 是否需要依赖外界模块
// 声明控制器
controllers: [AppController], // 控制器,负责校验和简单逻辑
// 声明数据业务服务
providers: [AppService], // 服务,data,负责复杂逻辑,调用数据库操作层,提供服务
})
// 装饰 AppModule
export class AppModule {}
注释里那句「复杂,有一份说明书,照着做」其实是讲 NestJS 自己的一套开发规范------这套规范确实复杂,但只要照着做,代码就规整,所有人写出来的 NestJS 项目结构都长一个样。装饰器(@Module、@Controller、@Injectable)就是这套规范落到代码里的形式:用 @ 标注每个类的角色和依赖关系,框架照着这份"说明书"把模块装配起来。
具体到 @Module({...}),这份"说明书"写了三件事:imports (依赖哪些外部模块)、controllers (注册哪些控制器)、providers(注册哪些服务)。框架启动时读这份说明书,把模块和它名下的 Controller、Provider 装配进容器,模块才真正生效。规范带来的不只是代码统一------依赖注入、模块化、可测试性都建立在这套约定之上。
@Module() 的配置项有三个关键字段:
imports:是否需要依赖外界模块(这里把TodosModule引进来)controllers:声明控制器(负责校验和简单逻辑)providers:声明数据业务服务(负责复杂逻辑、调用数据库操作层)
装饰器需要 TS + ES6 支持:装饰器是 ES7 提案的功能,TypeScript 默认支持。Nest.js 全面用 TS,所以装饰器写起来没有障碍。
@Controller + @Get:声明控制器和路由
根路由的控制器:
typescript
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
console.log('/ 的控制器');
// 需要返回什么内容给用户,都需要交给 service 来处理
// this -> 当前的 Module
return this.appService.getHello();
}
}
@Controller() 装饰这个类,告诉 Nest「这是一个控制器」。@Get() 装饰 getHello() 方法,告诉 Nest「这个方法处理 GET 请求,路径是根路由」。
控制器不负责所有事情。getHello() 里只做一件事:把请求转给 appService.getHello(),把返回值丢回去。注释里那句 this -> 当前的 Module 提醒我:this.appService 这个属性不是凭空来的,它是当前 Module 的容器自动注入的。这就引出下一个核心------依赖注入。
@Injectable:声明一个服务
app.service.ts:
typescript
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
// 返回值约束 返回数据给 controller 层
getHello(): string {
return 'Hello World!';
}
}
@Injectable() 装饰 AppService,告诉 Nest「这个类可以被容器管理,能被自动实例化和注入到任何需要它的地方」。getHello(): string 的返回值类型约束也是给 controller 层看的契约------告诉它「我返回的一定是 string」。
七、依赖注入:让框架替你 new 对象
这一节是这节课对我冲击最大的部分。前面三种设计模式都是「结构」,DI 是「运行机制」。
如果没有 DI
TodosController 的注释里写得很清楚:
typescript
// controller 要通过 service 拿到数据库中的数据,那怎么找到 todosService 呢?
// 1. import new 实例化 也就是每次请求都 new 一个实例,耦合紧、换实现要改 Controller、还无法 mock 测试
// 2. 也就是现在常用的方法
// 用 @Injectable() 把 Service 注册到 IOC 容器,容器创建并持有实例(默认单例),
// Controller 只声明依赖,容器把同一个实例注入进来,统一管理生命周期
两种方式对比:
- 手动 new :Controller 硬依赖具体 Service 类,每次请求自己
new TodosService(),耦合紧、换实现要改 Controller、无法 mock 测试,而且 Service 本可单例复用却被反复创建 - IOC 容器 :Service 用
@Injectable()注册到容器,容器创建并持有实例(默认单例);Controller 只声明「我需要TodosService」,容器把同一个实例注入进来,统一管理生命周期
依赖注入(DI,Dependency Injection):对象不自己创建依赖,而是把依赖声明出来,由外部容器负责提供。Nest.js 的 IOC 容器会管理服务实例和它们的生命周期(默认单例)。
IOC 容器(Inversion of Control) :控制反转容器。把「创建对象」的控制权从业务代码移出去,交给容器统一管理。控制器只声明「我需要
TodosService」,容器负责把实例递过来。
实战:在 TodosController 里用 DI
todos/todos.controller.ts:
typescript
import {
Controller,
Get,
Post,
Delete,
Patch,
Param,
Body
} from '@nestjs/common';
import { TodosService } from './todos.service';
import { type Todos } from './todos.service';
// 路由地址 /todos
@Controller('todos')
export class TodosController {
// service 会自动被注入到 controller 的 constructor 中
constructor(private readonly todosService: TodosService) {}
@Get()
findAll():Todos[] {
// /todos
console.log('/todos controller');
// controller 要通过 service 拿到数据库中的数据,那怎么找到 todosService 呢?
// 1. import new 实例化 也就是每次请求都 new 一个实例,太负责
// 2. 也就是现在常用的方法
// 自动注入到 IOC 容器中,每次请求都从容器中获取实例,让容器管理实例的生命周期,避免内存泄漏
return this.todosService.findAll();
}
@Get(':id')
// @Param 裯由参数,从 url 中直接获取参数
// 从 url 中获取参数,需要在 controller 中使用 @Param 装饰器
// 从 service 中获取参数,需要在 service 中使用 @Inject 装饰器
findOne(@Param('id') id: string):Todos {
console.log(id);
return this.todosService.findOne(+id);
}
@Post()
create(@Body('title') title: string):Todos {
// 接受和校验用户输入的参数
return this.todosService.create(title);
}
@Delete(':id')
remove(@Param('id') id: string):{message: string} {
this.todosService.remove(+id);
return {message: '删除成功'};
}
// Patch 和 Put 都是更新数据,但是 Put 会覆盖所有数据,而 Patch 只会更新需要更新的字段
@Patch(':id')
update(@Param('id') id: string, @Body() patch: Partial<Todos>):{message: string} {
return this.todosService.update(Number(id), patch);
}
}
关键就一行:
typescript
constructor(private readonly todosService: TodosService) {}
TodosController 不 new TodosService,它只在构造函数里声明「我需要 TodosService」。Nest 的 IOC 容器看到这个声明,就会在创建 TodosController 实例时,自动把 TodosService 的实例递进去。
这就是为什么 TodosService 上要加 @Injectable()------它得先被容器认领,容器才能在需要它的地方把它递出去。
八、Restful 思想 + Todos CRUD 实战
RESTful 是什么
RESTful :一种网络服务的架构风格。把系统里的一切都看作「资源」,用 URL 定位资源、用 HTTP 方法表达对资源的操作。
/todos是一种资源,GET /todos表示「查所有」,POST /todos表示「新建一个」,PATCH /todos/:id表示「改一个」,DELETE /todos/:id表示「删一个」。
Nest.js 的 @Controller('todos') + @Get() / @Post() / @Patch() / @Delete() 装饰器组合,恰好把 RESTful 思想直接落到代码里。HTTP 方法本身就是语义的一部分。
TodosModule 负责组装
typescript
// Todos Module 的定义文件
import { Module } from '@nestjs/common';
import { TodosController } from './todos.controller';
import { TodosService } from './todos.service';
// 大型后端框架,MVC 架构
// 视图层 不能直接去数据库中查询数据
// 控制器层
// 服务层
// 数据库操作层
@Module({
imports: [], // 是否需要依赖外界
// 声明控制器
controllers: [TodosController], // 控制器,负责校验和简单逻辑
// 声明数据业务服务
providers: [TodosService], // 服务,data,负责复杂逻辑,调用数据库操作层,提供服务
})
// 装饰 TodosModule
export class TodosModule {}
TodosModule 当前没有外部模块依赖,所以 imports 是空数组。AppModule 再通过 imports: [TodosModule] 把这个业务模块接进整个应用。
五类路由:CRUD 全套
TodosController 提供了五类操作:
| HTTP 方法 | 路径 | 装饰器 | 参数来源 | 对应 Service 方法 | 语义 |
|---|---|---|---|---|---|
| GET | /todos |
@Get() |
无 | findAll() |
查所有 |
| GET | /todos/:id |
@Get(':id') |
@Param('id') |
findOne(+id) |
查一个 |
| POST | /todos |
@Post() |
@Body('title') |
create(title) |
新建 |
| DELETE | /todos/:id |
@Delete(':id') |
@Param('id') |
remove(+id) |
删除 |
| PATCH | /todos/:id |
@Patch(':id') |
@Param('id') + @Body() |
update(Number(id), patch) |
部分更新 |
笔记里有一段对比要记一下:
typescript
// Patch 和 Put 都是更新数据,但是 Put 会用请求体替换整个资源,而 Patch 只会更新需要更新的字段
PATCH vs PUT :两者都能更新数据,但
PUT表示「用请求体替换整个资源」(没传的字段会被重置或丢失),PATCH只更新请求体里提供的字段,其他字段不动。这就是为什么update方法参数是Partial<Todos>------表示「Todos 的部分字段」。
Partial<Todos>:TypeScript 工具类型,把Todos所有字段变成可选。适合表达「部分更新」这种请求体------客户端可能只传{ completed: true },不传title。
@Param和@Body的分工 :@Param('id')从 URL 路径参数里取值(/todos/5里的5),@Body('title')从请求体里取值(POST/PATCH 携带的 JSON 字段)。注意+id和Number(id)是把字符串 id 转成数字------URL 参数进来时都是 string。
TodosService:业务逻辑都在这里
这次还没接数据库,数据先存在内存数组里。todos/todos.service.ts 完整代码:
typescript
import {
Injectable, // 注入,把一个类变成可以被 Nest 容器管理的服务,从而能被自动实例化和注入到任何需要它的地方
NotFoundException, // 404 错误
} from '@nestjs/common';
export interface Todos {
id: number;
title: string;
completed: boolean;
}
let todos: Todos[] = [
{ id: 1, title: '学习 nest.js', completed: false },
{ id: 2, title: '学习 angular', completed: true },
{ id: 3, title: '学习 vue', completed: false },
];
@Injectable()
export class TodosService {
findAll():Todos[] {
return todos;
}
findOne(id: number):Todos {
const todo = todos.find(t => t.id === id);
// 后端开发中,严谨稳定是十分重要的,所以需要容错模块
if (!todo) {
// 容错处理
throw new NotFoundException(`Todo ${id} 鲸鲸没找到`);
}
return todo;
}
create(title: string):Todos {
const todo = { id: todos.length + 1, title, completed: false };
todos.push(todo);
return todo;
}
remove(id: number):{message: string} {
const index = todos.findIndex(t => t.id === id);
if (index === -1) {
throw new NotFoundException(`Todo ${id} 鲸鲸没找到`);
}
todos.splice(index, 1);
return {message: '删除成功'};
}
update(id: number, patch: Partial<Todos>):{message: string} {
const todo = this.findOne(id);
Object.assign(todo, patch);
return {message: '更新成功'};
}
}
五个方法的逻辑要点:
findAll():直接返回整个todos数组。findOne(id):用Array.find找到对应 id 的 todo。找不到就抛NotFoundException------这就是容错处理,不能让客户端收到一个模棱两可的undefined。create(title):新建一个completed: false的 todo,id用todos.length + 1(简单粗暴,不是生产级方案),push进数组后返回新建的对象。remove(id):先findIndex找索引,找不到抛异常。找到就splice(index, 1)删掉,返回{message: '删除成功'}。update(id, patch):先调this.findOne(id)拿到原数据(找不到会抛异常),再用Object.assign(todo, patch)把patch里的字段合并进去。
Object.assign 这里很巧妙------它直接修改原 todo 对象,因为 findOne 返回的是数组里的引用。Object.assign(todo, patch) 的语义是:patch 里有的字段覆盖 todo 同名字段,patch 里没有的字段保留不动。这正好就是 PATCH 的语义------只更提供的字段。
九、异常处理:NotFoundException + try/catch/finally
笔记最后一段专门讲了后端报错怎么处理。
后端开发中,严谨稳定是十分重要的,所以需要容错模块。
Nest.js 提供了内置的错误类,NotFoundException 是其中一个,对应 HTTP 404。代码里两处用到了它:
typescript
// findOne 里
if (!todo) {
throw new NotFoundException(`Todo ${id} 鲸鲸没找到`);
}
// remove 里
if (index === -1) {
throw new NotFoundException(`Todo ${id} 鲸鲸没找到`);
}
NotFoundException 不是随便抛个 Error。Nest 会把它标准化成错误响应,里面至少包含:
statusCode:HTTP 状态码(这里是 404)message:错误消息(这里是我自定义的「Todo ${id} 鲸鲸没找到」)
NotFoundException:Nest.js 内置的 HTTP 异常类,对应 404 状态码。继承自HttpException。Nest 会自动把它转换成标准格式的错误响应返回给客户端。
try/catch/finally 的提醒
还对 try / catch / finally 做了提醒:
try catch finallyts是单线程,线程长时间处于错误情况,线程会挂nestjs提供了各种错误类,标准化错误输出statusCode状态码message消息
try/catch/finally :JavaScript 异常处理三件套。try包住可能出错的代码,catch捕获异常,finally无论是否出错都执行(通常用来收尾,比如关闭文件、释放连接)。
TS 单线程:JavaScript 和 TypeScript 运行在单线程环境里。一个未捕获的异常可能让整个线程挂掉,整个后端服务就停了。所以后端必须做容错------能捕获的异常要捕获,捕获后该走错误响应就走错误响应,不能让线程长时间处于错误状态。
十、我现在怎么理解这节课
写完笔记,我回头看 Nest.js 这套东西,能画一条主线了:
三种设计模式 + DI + RESTful,五件事互相咬合。
- 工厂模式 解决「应用怎么被创建」------
NestFactory.create(AppModule) - 分层(MVC)架构 解决「代码怎么分层」------Controller → Service → Repository(这是 MVC 在后端 API 场景的演化,View 被替代成 JSON 序列化)
- 装饰器机制 解决「分层信息怎么声明」------
@Module/@Controller/@Injectable/@Get等 - 依赖注入 解决「Controller 和 Service 之间怎么传递对象」------容器管对象,控制器只声明需要什么
- RESTful 思想 解决「接口怎么设计」------HTTP 方法 + 资源路径直接表达操作语义
这五件事不是平行的。它们互相依赖:没有装饰器,就没法声明模块和路由;没有 DI,控制器拿不到 service;没有分层架构,职责无从分开;没有工厂模式,应用创建过程和具体实现耦合;没有 RESTful,五类 CRUD 操作就没有统一的语义。
课堂上那个「蜜雪冰城」的类比,从工厂模式一路串到 Nest.js 工厂创建应用,再到 DI 容器替你 new 对象------本质都是「调用方不该管创建细节」。这个共性是我这节课最值得记住的认知。
Vibe Coding 视角
在 AI 时代,我可以让 Claude Code 30 秒生成一个 TodosController,也可以让它补出全套 CRUD。但如果我不懂模块边界、控制器职责、服务层和依赖注入,生成的代码越多,项目越难接手。
框架的价值,最后还是要靠我理解这些结构之后才能发挥出来。AI 写得快,不代表我懂;我懂了,才知道让 AI 写什么、写完怎么接、接到哪个 Module。
Nest.js 满屏 @ 装饰器看起来吓人,但拆完发现它就是这五件事的语法糖。装饰器不是装饰,是给框架看的说明书。
术语速查
- Nest CLI :Nest.js 的命令行工具,用于安装、初始化、启动和管理 Nest 项目。
nest new、nest start都来自它。 - Provider :可以被 Nest 容器管理并注入的服务或对象,
Service是最常见的一类 Provider。 - CRUD:Create、Read、Update、Delete,增删改查。绝大部分业务系统的底层就是这四个动作。
- REST:通过 HTTP 方法和资源路径表达接口操作的一种 Web API 风格。
- WebSocket:支持客户端与服务端持续双向通信的协议形态。
- 微服务:把一个较大的应用拆成多个可以独立开发、部署和通信的服务。
- rxjs:响应式编程库,Nest 内部很多异步流程基于它。
- 23 种设计模式:GoF 总结的面向对象设计模式集合,分创建型、结构型、行为型三类。工厂模式属于创建型,装饰器模式属于结构型。