3 种设计模式 + DI:Nest.js 后端第一课

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 里默认带好了一系列脚本:buildstartstart:dev(watch 模式)、start:debugstart:prodnode dist/main)、linttesttest:e2etest:cov,以及对应的依赖。

关键依赖有几个值得记一下:

  • @nestjs/common:Nest 的核心常用装饰器和工具,@Module@Controller@InjectableNotFoundException 都从这里来
  • @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();

这套代码里我注意到几件事:

  1. 三个产品类 IceCreamLemonTeaMilkTea 都实现了相同的 show() 接口。这是工厂模式能成立的前提------产品必须面向同一个抽象类型(接口/抽象类),让客户端能依赖"抽象产品"而非具体产品类。这正是依赖倒置:客户端面向抽象编程,工厂负责产出具体实例。
  2. MixueFactory 是个静态方法工厂,调用方只需要 MixueFactory.createMixue('iceCream'),根本不用知道 IceCream 的构造细节。
  3. switch 里有一个 default 分支,遇到未知类型会 throw new Error('未知的混雪类型')。工厂不是「传什么都返回一个东西」,而是明确告诉调用方:这个配置生产不了。
  4. 这里有个容易混淆的点:不负责实例创建的是调用方,不是工厂 。调用方不直接 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();

注释里藏着两个关键认知:

  1. NestFactory.create(AppModule) 不是普通的 new。Nest 后端形态多变(REST / WebSocket / 微服务),直接 new 会让调用方和具体实现耦合。工厂把「创建哪个应用 / 怎么创建」封装起来,调用方只传配置(AppModule)。
  2. 请求进入 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 只声明依赖,容器把同一个实例注入进来,统一管理生命周期

两种方式对比:

  1. 手动 new :Controller 硬依赖具体 Service 类,每次请求自己 new TodosService(),耦合紧、换实现要改 Controller、无法 mock 测试,而且 Service 本可单例复用却被反复创建
  2. 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) {}

TodosControllernew 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 字段)。注意 +idNumber(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: '更新成功'};
    }
}

五个方法的逻辑要点:

  1. findAll() :直接返回整个 todos 数组。
  2. findOne(id) :用 Array.find 找到对应 id 的 todo。找不到就抛 NotFoundException------这就是容错处理,不能让客户端收到一个模棱两可的 undefined
  3. create(title) :新建一个 completed: false 的 todo,idtodos.length + 1(简单粗暴,不是生产级方案),push 进数组后返回新建的对象。
  4. remove(id) :先 findIndex 找索引,找不到抛异常。找到就 splice(index, 1) 删掉,返回 {message: '删除成功'}
  5. 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 finally
  • ts 是单线程,线程长时间处于错误情况,线程会挂
  • 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 newnest start 都来自它。
  • Provider :可以被 Nest 容器管理并注入的服务或对象,Service 是最常见的一类 Provider。
  • CRUD:Create、Read、Update、Delete,增删改查。绝大部分业务系统的底层就是这四个动作。
  • REST:通过 HTTP 方法和资源路径表达接口操作的一种 Web API 风格。
  • WebSocket:支持客户端与服务端持续双向通信的协议形态。
  • 微服务:把一个较大的应用拆成多个可以独立开发、部署和通信的服务。
  • rxjs:响应式编程库,Nest 内部很多异步流程基于它。
  • 23 种设计模式:GoF 总结的面向对象设计模式集合,分创建型、结构型、行为型三类。工厂模式属于创建型,装饰器模式属于结构型。
相关推荐
程序员cxuan1 小时前
一招教你在 Codex 中开启 1M 上下文
人工智能·后端·程序员
IT_陈寒2 小时前
Vite热更新失效?八成是这个配置在搞鬼
前端·人工智能·后端
qq_22589174662 小时前
基于Flask的空气质量监测与预测分析系统
开发语言·后端·python·信息可视化·django·flask
覆东流2 小时前
4.Java运算符与表达式
java·开发语言·后端
马丁玩编程2 小时前
GitHub 3.5k Star 之后,Ragent AI 框架新版本来了
后端·面试·github
微石科技2 小时前
医疗设备维保如何从被动维修转向预防管理?宁波微石科技让运行数据提前发信号
java·后端·struts
AINative软件工程2 小时前
LLM 应用的 Warm-Up 工程实践:冷启动延迟从 12 秒砍到 800ms 的 5 个工程手段
后端·llm·ai编程
SomeB1oody2 小时前
【RustyML入门】5.1. 回归指标
开发语言·后端·机器学习·rust·教程
卷无止境3 小时前
手写 SQL 在 Tortoise ORM 里到底能派上什么用场
后端·python·fastapi