🔥 NestJS 从零到实战:一个 Todos CRUD 搞懂企业级后端框架的核心设计

🔥 NestJS 从零到实战:一个 Todos CRUD 搞懂企业级后端框架的核心设计

摘要:本文以一个 Todos 任务管理 API 为实战项目,不只告诉你"怎么用",更带你深入理解工厂模式、依赖注入、装饰器模式的底层实现原理。读完这篇,面试官问你"NestJS 怎么工作的",你能从源码层面讲清楚。


📌 前言

最近在学后端,看了一圈框架:Express 太自由、Koa 太轻量、Django 不是 JS 生态。最后选了 NestJS --- Node.js 生态里最接近 Spring Boot 的企业级框架。

学完之后最大的感受是:NestJS 不是在教你写接口,而是在教你写「可维护的后端系统」。

但很多教程只告诉你"这样写就行",不告诉你"为什么这样写"。这篇文章用一个 Todos CRUD 项目,把 NestJS 最核心的设计思想从源码层面讲清楚。


🎯 本文适合谁

  • 想入门 NestJS 但被官方文档劝退的前端同学
  • 准备后端面试,需要能讲清楚框架原理
  • 想理解「模块化」「依赖注入」「装饰器」底层是怎么实现的开发者

📚 一、工厂模式 --- NestFactory.create() 到底做了什么

先看用法

typescript 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

一行 NestFactory.create(AppModule) 就启动了整个应用。看起来很简单,但背后做了很多事情。

工厂模式的本质

工厂模式的核心思想:你不需要知道对象是怎么创建的,只需要告诉工厂你要什么。

用蜜雪冰城类比:

arduino 复制代码
你想喝奶茶 → 不自己做(流程代码 NO)→ 找蜜雪冰城(工厂)
NestFactory.create(AppModule) → 不手动 new 各种类 → 工厂帮你生产

但这个类比太浅了。我们来看 NestFactory 内部到底做了什么。

NestFactory.create() 的内部流程

⚠️ 以下是基于 @nestjs/core 源码逻辑重构的伪代码,并非逐行摘录。目的是帮你理解框架内部的执行顺序和核心机制。

typescript 复制代码
// NestFactory.create() 简化版
export class NestFactory {
  static async create(module, serverOptions?) {
    // 第一步:创建 IoC 容器(依赖注入的核心)
    const container = new NestContainer();

    // 第二步:解析模块,把 AppModule 及其所有依赖注册到容器
    // 这一步会递归遍历 imports、controllers、providers
    await container.addModule(module);

    // 第三步:实例化所有 Controller 和 Service
    // 容器会根据装饰器的元数据,自动 new 并注入依赖
    const instanceLoader = new InstanceLoader(container);
    await instanceLoader.createInstancesOfDependencies();

    // 第四步:创建 HTTP 服务器(Express/Fastify)
    const httpServer = this.createHttpServer(serverOptions);

    // 第五步:注册路由 --- 把 Controller 的方法绑定到对应的 HTTP 路由
    const routesResolver = new RoutesResolver(container);
    routesResolver.resolve(httpServer);

    return new NestApplication(httpServer, container);
  }
}

关键步骤拆解

vbnet 复制代码
NestFactory.create(AppModule)
  ├── 1. 创建 IoC 容器(空的依赖注册表)
  ├── 2. 解析 AppModule
  │     ├── 发现 imports: [TodosModule]
  │     ├── 递归解析 TodosModule
  │     │     ├── 发现 controllers: [TodosController]
  │     │     └── 发现 providers: [TodosService]
  │     └── 把所有类注册到容器
  ├── 3. 实例化依赖
  │     ├── new TodosService() → 放入容器
  │     └── new TodosController(todosService) → 从容器取出注入
  ├── 4. 创建 HTTP 服务器
  └── 5. 注册路由
        ├── GET /todos → TodosController.findAll
        ├── GET /todos/:id → TodosController.findOne
        ├── POST /todos → TodosController.create
        └── ...

面试话术

NestFactory.create() 内部做了五件事:创建 IoC 容器、递归解析模块依赖、实例化所有类并注入依赖、创建 HTTP 服务器、注册路由。它本质上是一个工厂方法,把复杂的启动流程封装成一个调用,开发者只需要传入根模块就行。这就是工厂模式的价值 --- 封装创建过程,暴露简单接口


📚 二、依赖注入 --- 不手动 new,框架怎么知道要注入什么

先看现象

typescript 复制代码
// TodosController --- 构造函数里声明了依赖
export class TodosController {
  constructor(private readonly todosService: TodosService) {}
  // ...
}

我们从来没有写过 new TodosService(),但 todosService 就能用了。为什么?

依赖注入的三要素

markdown 复制代码
1. 声明依赖 --- 构造函数参数
2. 注册服务 --- @Injectable() + Module.providers
3. 容器解析 --- IoC 容器自动创建和注入

底层原理:Reflect Metadata

NestJS 用 TypeScript 的 reflect-metadata 来实现依赖注入。这是关键中的关键。

这里有两个独立的机制,必须分清楚:

机制一:emitDecoratorMetadata(TypeScript 编译器行为)

tsconfig.json 中开启了 emitDecoratorMetadata: true,只要类上有任何装饰器,TypeScript 编译器就会自动发射类型元数据:

typescript 复制代码
// 你写的代码
export class TodosController {
  constructor(private readonly todosService: TodosService) {}
}

// TypeScript 编译后(只要类上有任何装饰器,就会自动发射):
__metadata("design:paramtypes", [TodosService])
// 意思是:这个类的构造函数参数类型是 [TodosService]

运行时可以通过 Reflect 读取:

typescript 复制代码
const dependencies = Reflect.getMetadata('design:paramtypes', TodosController);
// dependencies = [TodosService]  ← 拿到了参数的类型信息!

机制二:@Injectable()(NestJS 自己的标记行为)

@Injectable() 的作用是给类打上一个 injectable: true 的标记,告诉 NestJS "这个类可以被容器管理":

typescript 复制代码
@Injectable()
export class TodosService { ... }

// 等价于:
Reflect.defineMetadata('injectable', true, TodosService);

两者的关系

java 复制代码
emitDecoratorMetadata  → 告诉框架"这个类需要哪些依赖"(类型信息)
@Injectable()          → 告诉框架"这个类可以被容器管理"(标记信息)

它们是独立的。即使你不用 @Injectable(),只要有其他装饰器,design:paramtypes 照样会被发射。但没有 @Injectable() 标记的类不会被 NestJS 容器管理,注入不会生效。

IoC 容器的工作流程

java 复制代码
1. 扫描所有 @Injectable() 的类 → 注册到容器
2. 对每个需要注入的类:
   a. Reflect.getMetadata('design:paramtypes', Class) → 获取依赖列表
   b. 检查容器里有没有这个依赖
   c. 如果没有 → 先创建依赖(递归)
   d. 如果有 → 取出来,传入构造函数
3. 完成所有实例化

用代码模拟:

typescript 复制代码
// 简化版 IoC 容器
class Container {
  private instances = new Map();

  // 注册
  register(token, Class) {
    // 防重复注册
    if (this.instances.has(token)) return;

    // 获取构造函数的参数类型
    const deps = Reflect.getMetadata('design:paramtypes', Class) || [];

    // 递归创建依赖
    const instances = deps.map(dep => this.resolve(dep));

    // 创建实例,注入依赖
    const instance = new Class(...instances);

    this.instances.set(token, instance);
  }

  // 解析
  resolve(token) {
    if (!this.instances.has(token)) {
      this.register(token, token);
    }
    return this.instances.get(token);
    // 注:真实 NestJS 容器会检测循环依赖(A→B→A)
    // 检测到时抛出 CircularDependencyException,避免无限递归
  }
}

// 使用
const container = new Container();
container.register(TodosService, TodosService);
container.register(TodosController, TodosController);

// 此时 TodosController 内部已经有了 TodosService 的实例

为什么需要 DI?--- 对比手动创建

typescript 复制代码
// ❌ 没有 DI 的情况 --- 紧耦合
export class TodosController {
  private todosService: TodosService;

  constructor() {
    // 手动创建,耦合了具体实现
    this.todosService = new TodosService();
  }
}

// ❌ 如果 TodosService 的构造函数变了(需要注入 LoggerService)
// TodosController 也要改!改一个地方,所有用到它的地方都要改

// ✅ 有 DI 的情况 --- 松耦合
export class TodosController {
  constructor(private readonly todosService: TodosService) {}
  // 不关心怎么创建的,只关心"我需要一个 TodosService"
}

// ✅ TodosService 的构造函数变了,TodosController 完全不用改
// 框架自动处理依赖链

面试话术

NestJS 的依赖注入基于 reflect-metadata 实现。当我们在构造函数里声明 todosService: TodosService 时,TypeScript 编译器会把参数类型信息存储到 Reflect 元数据中。框架启动时,IoC 容器读取这些元数据,自动创建 TodosService 实例并注入到 Controller 中。如果 Service 还有其他依赖,容器会递归解析,直到所有依赖都创建完毕。这种机制让代码松耦合、可测试、可维护。


📚 三、语法糖到底是什么 --- 从 JS 到装饰器

在讲装饰器之前,必须先搞清楚一个概念:语法糖(Syntactic Sugar)

什么是语法糖?

语法糖是编程语言提供的一种简写形式。它让你用更简洁、更易读的语法写代码,但编译器/解释器会把它翻译成等价的、更啰嗦的原始写法。

一句话总结:语法糖 = 换了个好看的皮,骨子里还是同一个东西。

例子 1:箭头函数 --- 最常见的语法糖

javascript 复制代码
// 你写的(语法糖)
const add = (a, b) => a + b;

// 等价的原始写法
const add = function(a, b) {
  return a + b;
};

箭头函数只是把 function 关键字省掉了,加了个 =>。简单场景下两者可以互换,但它们并不完全一样

特性 普通函数 箭头函数
this 绑定 动态绑定(调用时决定) 词法绑定(定义时继承外层)
arguments 对象 ✅ 有 ❌ 没有
可否用作构造函数 ✅ 可以 new ❌ 不可以 new
prototype 属性 ✅ 有 ❌ 没有

最常见的坑:在对象方法中用箭头函数,this 不会指向对象本身。

javascript 复制代码
const obj = {
  name: 'NestJS',
  // ❌ 箭头函数的 this 继承外层(window/undefined),不是 obj
  greet: () => { console.log(this.name); },
  // ✅ 普通函数的 this 指向调用者(obj)
  greet2() { console.log(this.name); }
};

例子 2:解构赋值 --- 从对象里取值的语法糖

javascript 复制代码
// 你写的(语法糖)
const { name, age } = user;

// 等价的原始写法
const name = user.name;
const age = user.age;

解构赋值让你不用重复写 user.,但本质就是属性访问。

例子 3:模板字符串 --- 拼接字符串的语法糖

javascript 复制代码
// 你写的(语法糖)
const msg = `Hello, ${name}! You are ${age} years old.`;

// 等价的原始写法
const msg = 'Hello, ' + name + '! You are ' + age + ' years old.';

反引号 ${} 只是让拼接更易读,本质还是字符串连接。

例子 4:展开运算符 --- 复制/合并数组和对象的语法糖

javascript 复制代码
// 你写的(语法糖)
const newArr = [...arr1, ...arr2];
const newObj = { ...oldObj, name: 'new' };

// 等价的原始写法
const newArr = arr1.concat(arr2);
const newObj = Object.assign({}, oldObj, { name: 'new' });

例子 5:class 语法 --- 面向对象的语法糖

javascript 复制代码
// 你写的(语法糖)
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a sound.`;
  }
}

// 等价的原始写法(ES5 原型链)
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() {
  return this.name + ' makes a sound.';
};

ES6 的 class 底层仍然基于原型链实现,但引入了一些新的约束和行为差异:

特性 ES5 原型链 ES6 class
变量提升 function 声明会提升 ❌ 不可提升(有 TDZ)
严格模式 需手动声明 ✅ class 内部自动严格模式
方法可枚举 ✅ 可枚举 ❌ 不可枚举
必须 new 不强制(可当普通函数调用) ✅ 必须 new,否则报错

所以 class 不只是"好看的皮",它在行为上有实际差异。说"底层还是 prototype"没有错,说"只是语法糖"则过于简化。

例子 6:async/await --- Promise 的语法糖

javascript 复制代码
// 你写的(语法糖)
async function fetchUser() {
  const res = await fetch('/api/user');
  const data = await res.json();
  return data;
}

// 等价的原始写法(Promise 链)
function fetchUser() {
  return fetch('/api/user')
    .then(function(res) { return res.json(); })
    .then(function(data) { return data; });
}

async/await 让异步代码看起来像同步代码,但本质还是 Promise。

例子 7:可选链 --- 安全访问嵌套属性的语法糖

javascript 复制代码
// 你写的(语法糖)
const city = user?.address?.city;

// 等价的原始写法
const city = (user && user.address && user.address.city) || undefined;

例子 8:空值合并 --- 提供默认值的语法糖

javascript 复制代码
// 你写的(语法糖)
const port = config.port ?? 3000;

// 等价的原始写法
const port = (config.port !== null && config.port !== undefined)
  ? config.port
  : 3000;

语法糖的本质

复制代码
语法糖的本质:
┌─────────────────────────────────────────────┐
│  你写的代码(简洁、易读)                      │
│         ↓  编译器/解释器翻译                   │
│  等价的原始写法(啰嗦、但功能相同)              │
└─────────────────────────────────────────────┘

语法糖不是新功能,是老功能的新写法。 它让代码更简洁、更易读,但不改变底层逻辑。

现在回到装饰器 --- TypeScript 的语法糖

理解了语法糖的概念,装饰器就很好理解了。

typescript 复制代码
// 你写的(语法糖)
@Injectable()
export class TodosService { ... }

编译后(当 emitDecoratorMetadata 开启时):

javascript 复制代码
var TodosService = /** @class */ (function () {
  function TodosService() { ... }
  // __decorate 会把 decorators 数组中的函数依次执行
  // __metadata 虽然在同一个数组里,但它不是装饰器,而是类型元数据
  TodosService = __decorate([
    Injectable(),                         // NestJS 的标记:injectable = true
    __metadata("design:paramtypes", [])   // TypeScript 自动发射:构造函数参数类型
  ], TodosService);
  return TodosService;
}());

⚠️ 注意:__metadata("design:paramtypes", []) 这行不是 @Injectable() 产生的,而是 TypeScript 的 emitDecoratorMetadata 编译选项自动发射的。只要类上有任何装饰器 ,TypeScript 就会自动发射 design:typedesign:paramtypesdesign:returntype 三种元数据。@Injectable() 的作用是标记 injectable: true,两者是独立的机制。虽然它们都在 __decorate 的同一个数组中,但 __metadata 会被 __decorate 内部识别为非函数而跳过执行。

TypeScript 编译器生成的 __decorate 辅助函数简化版:

javascript 复制代码
// 简化版(注意:真实实现更复杂)
// 签名:__decorate(decorators, target, key?, descriptor?)
// 类装饰器只传 target;方法/属性装饰器还会传 key 和 descriptor
function __decorate(decorators, target, key, descriptor) {
  // 关键:装饰器是从后往前执行的!
  for (var i = decorators.length - 1; i >= 0; i--) {
    var decorator = decorators[i];
    if (decorator) {
      // 根据参数数量决定调用方式:
      // 类装饰器(无 key):decorator(target)
      // 方法装饰器(有 key):decorator(target, key, descriptor)
      var result = (key !== undefined)
        ? decorator(target, key, descriptor)
        : decorator(target);
      if (result) target = result; // 类装饰器可以替换目标类
    }
  }
  return target;
}

💡 为什么是逆序?因为装饰器的语义是"从内到外"包装。如果多个装饰器叠加,最靠近类/方法的装饰器先执行,最外层的后执行。逆序遍历保证了这个语义。

所以 @Injectable() 就是 Injectable()(TodosService) 的语法糖。

具体来说:

typescript 复制代码
@Injectable()
export class TodosService { ... }

// 等价于:
Injectable()(TodosService);

// 展开来看:
// 第一步:调用 Injectable(),返回一个函数
const fn = Injectable();
// fn = (target) => { Reflect.defineMetadata('injectable', true, target); }

// 第二步:用目标类调用这个函数
fn(TodosService);
// 等价于:
Reflect.defineMetadata('injectable', true, TodosService);

用你熟悉的语法糖类比

语法糖 你写的 等价的原始写法 注意事项
箭头函数 (a, b) => a + b function(a, b) { return a + b; } this 绑定方式不同!
解构赋值 const { name } = user const name = user.name
模板字符串`` Hi ${name} 'Hi ' + name
async/await await fetch(url) fetch(url).then(...) 错误处理行为有差异
class class A { method() {} } A.prototype.method = function() {} 不可提升、严格模式、方法不可枚举
装饰器 @Injectable() Injectable()(Target) 逆序执行,类型签名不同

装饰器和箭头函数、解构赋值一样,都是语法糖 --- 好看的皮,相同的骨

类装饰器 --- @Injectable() 的实现

typescript 复制代码
// @Injectable() 的简化实现
export function Injectable(): ClassDecorator {
  // 返回一个函数,这个函数接收目标类
  return (target: Function) => {
    // 在目标类上存储元数据,标记为"可注入"
    Reflect.defineMetadata('injectable', true, target);
  };
}

使用时:

typescript 复制代码
@Injectable()
export class TodosService { ... }

// 等价于:
Injectable()(TodosService);
// 即:
(target) => { Reflect.defineMetadata('injectable', true, target); }(TodosService);

方法装饰器 --- @Get(':id') 的实现

typescript 复制代码
// @Get() 的简化实现
export function Get(path?: string): MethodDecorator {
  // 返回一个函数,接收三个参数:
  // target --- 类的原型
  // propertyKey --- 方法名
  // descriptor --- 方法的属性描述符
  return (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
    // 存储路由元数据
    Reflect.defineMetadata('path', path, descriptor.value);
    Reflect.defineMetadata('method', 'GET', descriptor.value);
  };
}

使用时:

typescript 复制代码
@Get(':id')
findOne(@Param('id') id: string): Todo { ... }

// 等价于:
Get(':id')(TodosController.prototype, 'findOne', descriptor);
// 即:
(target, propertyKey, descriptor) => {
  Reflect.defineMetadata('path', ':id', descriptor.value);
  Reflect.defineMetadata('method', 'GET', descriptor.value);
}(TodosController.prototype, 'findOne', descriptor);

参数装饰器 --- @Param('id') 的实现

typescript 复制代码
// @Param() 的简化实现
export function Param(property?: string): ParameterDecorator {
  // 返回一个函数,接收四个参数:
  // target --- 类的原型
  // propertyKey --- 方法名
  // parameterIndex --- 参数在参数列表中的位置
  return (target: Object, propertyKey: string, parameterIndex: number) => {
    // 获取已有的参数元数据
    const existingParams = Reflect.getMetadata('params', target, propertyKey) || [];

    existingParams.push({
      index: parameterIndex,
      type: 'param',
      property: property,
    });

    // 存储回去
    Reflect.defineMetadata('params', existingParams, target, propertyKey);
  };
}

使用时:

typescript 复制代码
findOne(@Param('id') id: string): Todo { ... }

// 等价于:
Param('id')(TodosController.prototype, 'findOne', 0);
// 第三个参数 0 表示 id 是第一个参数

完整的装饰器类型

类型 签名 用途 示例
类装饰器 (target: Function) => void 修改类的行为 @Injectable() @Module()
方法装饰器 (target, key, descriptor) => void 修改方法的行为 @Get() @Post()
属性装饰器 (target, key) => void 修改属性 @Inject()
参数装饰器 (target, key, index) => void 标记参数 @Param() @Body()

装饰器的执行顺序

typescript 复制代码
@Module({                  // 1. 类装饰器(从上到下)
  controllers: [TodosController],
  providers: [TodosService],
})
export class TodosModule {
  @Get(':id')              // 2. 方法装饰器(从上到下)
  findOne(
    @Param('id') id: string // 3. 参数装饰器(从右到左!)
  ) {}
}

执行顺序:

kotlin 复制代码
1. 参数装饰器 @Param('id') --- 从右到左(本例只有一个)
2. 方法装饰器 @Get(':id')
3. 类装饰器 @Module({ ... })

面试话术

TypeScript 装饰器本质上就是函数语法糖。@Injectable() 编译后就是 Injectable()(TodosService) --- 先调用 Injectable() 返回一个函数,再用目标类调用这个函数。函数内部通过 Reflect.defineMetadata() 存储元数据。框架启动时读取这些元数据,就知道哪些类是服务、哪些方法是路由、哪些参数需要从请求中提取。NestJS 把装饰器用到了极致 --- 类装饰器声明模块、方法装饰器声明路由、参数装饰器声明参数来源,所有元数据都通过 reflect-metadata 统一管理。


📚 四、实战:Todos CRUD 完整实现

理解了底层原理,我们来看完整的业务代码。

Service 层 --- 业务逻辑

typescript 复制代码
import { Injectable, NotFoundException } from '@nestjs/common';

export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

let todos: Todo[] = [
  { id: 1, title: '学习 NestJS', completed: false },
  { id: 2, title: '学习 CRUD', completed: true },
];
let nextId = 3;

@Injectable()
export class TodosService {
  findAll(): Todo[] {
    return todos;
  }

  findOne(id: number): Todo {
    const todo = todos.find(todo => todo.id === id);
    if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
    return todo;
  }

  create(title: string): Todo {
    const todo: Todo = { id: nextId++, title, completed: false };
    todos.push(todo);
    return todo;
  }

  remove(id: number): void {
    const index = todos.findIndex(t => t.id === id);
    if (index === -1) throw new NotFoundException(`Todo ${id} 不存在`);
    todos.splice(index, 1);
  }

  update(id: number, patch: Partial<Todo>): Todo {
    const todo = this.findOne(id);
    Object.assign(todo, patch);
    // 注意:Object.assign 会直接修改原数组中的对象引用(mutation)
    // 本例为教学简化,生产环境建议用展开运算符创建新对象:
    // const updated = { ...todo, ...patch };
    // todos[index] = updated;
    return todo;
  }
}

Controller 层 --- 路由 + 参数

typescript 复制代码
import { Controller, Get, Post, Delete, Patch, Param, Body } from '@nestjs/common';
import { TodosService } from './todos.service';
import type { Todo } from './todos.service';

@Controller('todos')
export class TodosController {
  constructor(private readonly todosService: TodosService) {}

  @Get()
  findAll(): Todo[] {
    return this.todosService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string): Todo {
    return this.todosService.findOne(+id);
  }

  @Post()
  create(@Body('title') title: string): Todo {
    return this.todosService.create(title);
  }

  @Delete(':id')
  remove(@Param('id') id: string): { message: string } {
    this.todosService.remove(Number(id));
    return { message: '删除成功' };
  }

  @Patch(':id')
  update(
    @Param('id') id: string,
    @Body() patch: Partial<Todo>
  ): Todo {
    return this.todosService.update(Number(id), patch);
  }
}

Module --- 组装

typescript 复制代码
import { Module } from '@nestjs/common';
import { TodosController } from './todos.controller';
import { TodosService } from './todos.service';

@Module({
  controllers: [TodosController],
  providers: [TodosService],
})
export class TodosModule {}

请求生命周期

kotlin 复制代码
客户端: GET /todos/1
    ↓
main.ts: NestFactory.create(AppModule) 启动的 HTTP 服务器
    ↓
路由匹配: GET /todos/:id → TodosController.findOne
    ↓
参数提取: @Param('id') → id = "1"(字符串)
    ↓
类型转换: +id → 1(数字)
    ↓
调用 Service: this.todosService.findOne(1)
    ↓
业务逻辑: todos.find(todo => todo.id === 1)
    ↓
找不到? → throw new NotFoundException → 框架返回 404
找到了? → return todo → 框架序列化为 JSON → 200 响应

📚 五、错误处理 --- 框架级的标准化

传统方式 vs NestJS 方式

typescript 复制代码
// ❌ 传统方式 --- try-catch,代码冗余
try {
  const todo = todos.find(t => t.id === id);
  if (!todo) {
    res.status(404).json({ statusCode: 404, message: 'Not Found' });
    return;
  }
  res.json(todo);
} catch (err) {
  res.status(500).json({ statusCode: 500, message: 'Internal Error' });
}

// ✅ NestJS 方式 --- 声明式,框架统一处理
findOne(id: number): Todo {
  const todo = todos.find(todo => todo.id === id);
  if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
  return todo;
}

框架内部怎么处理异常

arduino 复制代码
Service 层 throw new NotFoundException('Todo 99 不存在')
    ↓
NestJS 异常过滤器捕获
    ↓
提取 statusCode: 404
    ↓
生成标准响应:
{
  "statusCode": 404,
  "message": "Todo 99 不存在",
  "error": "Not Found"
}
    ↓
返回给客户端

内置异常类

异常类 状态码 场景
BadRequestException 400 参数校验失败
UnauthorizedException 401 未登录
ForbiddenException 403 无权限
NotFoundException 404 资源不存在
ConflictException 409 资源冲突(如重复创建)
InternalServerErrorException 500 服务器内部错误

📚 六、TypeScript 泛型在后端的应用

Partial --- PATCH 更新的利器

typescript 复制代码
@Patch(':id')
update(@Param('id') id: string, @Body() patch: Partial<Todo>): Todo {
  return this.todosService.update(Number(id), patch);
}

Partial<Todo> 的含义:

typescript 复制代码
// Todo 的原始类型
interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

// Partial<Todo> 变成
{
  id?: number;        // 可选
  title?: string;     // 可选
  completed?: boolean; // 可选
}

为什么 PATCH 需要 Partial?

PATCH 的语义是"部分更新" --- 只更新传入的字段,不传的保持原值。如果用完整的 Todo 类型,客户端必须传所有字段,这就变成了 PUT 的语义。

typescript 复制代码
// PATCH /todos/1
// Body: { "completed": true }  ← 只更新 completed

// Service 层
update(id: number, patch: Partial<Todo>): Todo {
  const todo = this.findOne(id);    // 找到原对象
  Object.assign(todo, patch);       // 只覆盖传入的字段
  return todo;                      // title 和 id 保持不变
}

💡 经验总结

1. 理解原理比会用更重要

面试官问"NestJS 怎么实现依赖注入",如果你只会说"在构造函数里声明就行",那是表面功夫。能讲清楚 reflect-metadata、IoC 容器、元数据存储,才是加分项。

2. 装饰器不神秘

@xxx 就是函数调用的语法糖。编译后变成 xxx(target) 的形式,内部通过 Reflect.defineMetadata() 存储元数据。理解了这一点,看 NestJS 的任何装饰器都不会懵。

3. 工厂模式封装了复杂性

NestFactory.create() 一行代码背后是:容器创建 → 模块解析 → 依赖实例化 → 服务器创建 → 路由注册。工厂模式的价值就是把这五步封装成一个调用。

4. Controller 只做转发

Controller 的职责是:接收请求 → 获取参数 → 调用 Service → 返回结果。不要在 Controller 里写业务逻辑

5. 错误处理要标准化

用 NestJS 内置的异常类,不要自己写 try-catch。标准化的错误输出让前端更容易处理,也让 API 更专业。


📊 RESTful API 设计速查

操作 HTTP 方法 路径 语义
查询所有 GET /todos 获取资源集合
查询单个 GET /todos/1 获取单个资源
创建 POST /todos 创建新资源
部分更新 PATCH /todos/1 更新部分字段
删除 DELETE /todos/1 删除资源

RESTful 核心原则

  • 资源用名词(todos),不用动词(getTodos ❌)
  • HTTP 方法表达动作(GET 查询、POST 创建、DELETE 删除)
  • 状态码表达结果(200 成功、404 未找到)

🔗 参考资料


💬 交流讨论

你在学 NestJS 的过程中遇到了什么问题?或者你觉得 NestJS 和其他框架相比有什么优劣?欢迎在评论区交流!


觉得有用?点个赞👍收藏⭐关注👆,后续会更新 NestJS 进阶内容:中间件、拦截器、管道、守卫、微服务架构!

相关推荐
吃饱了得干活1 小时前
从经典的三层架构到DDD:一次对“业务逻辑层”的解剖与重构
java·后端·架构
只一1 小时前
吃透 NestJS 核心|工厂模式 + 依赖注入 + Todo CRUD 实战,后端入门到上手
nestjs
XuCoder1 小时前
面试官:什么是覆盖索引?
后端
林太白1 小时前
What did Trea work help me with in this optimization process
前端·后端
Asize1 小时前
3 种设计模式 + DI:Nest.js 后端第一课
后端·设计模式·nestjs
程序员cxuan2 小时前
一招教你在 Codex 中开启 1M 上下文
人工智能·后端·程序员
IT_陈寒2 小时前
Vite热更新失效?八成是这个配置在搞鬼
前端·人工智能·后端
qq_22589174662 小时前
基于Flask的空气质量监测与预测分析系统
开发语言·后端·python·信息可视化·django·flask
覆东流2 小时前
4.Java运算符与表达式
java·开发语言·后端