NestJS 全栈 CRUD 实战:从 Module 拆分到 RESTful 七大装饰器,彻底搞懂后端接口工程
上篇我们用蜜雪冰城理解了工厂模式和装饰器模式,掌握了 NestJS 的设计哲学。这篇进入实战——用 NestJS 从零搭建一个完整的 Todos CRUD 接口。从根模块拆分子模块,到 Controller 的五大 HTTP 装饰器、Service 的业务逻辑与错误处理,再到
@Param@Body参数提取和Partial<T>类型技巧——一篇文章打通后端接口工程的完整链路。全文代码可直接运行,建议收藏后动手实践。
一、NestJS 开发流程全景
1.1 从单模块到多模块的演进
上篇:单模块架构(学习阶段)
src/
├── main.ts # 入口
├── app.module.ts # 根模块(所有功能堆在一起)
├── app.controller.ts # 根控制器
└── app.service.ts # 根服务
本篇:多模块架构(企业级)
src/
├── main.ts # 入口
├── app.module.ts # 根模块(imports 子模块)
├── app.controller.ts # 根控制器
├── app.service.ts # 根服务
└── todos/ # Todos 业务模块
├── todos.module.ts # 模块定义(组装 Controller + Service)
├── Todos.controller.ts # 控制器(路由 + 参数校验)
└── Todos.service.ts # 服务(业务逻辑 + 数据操作)
1.2 NestJS 模块开发约定
┌──────────────────────────────────────────────────────────┐
│ NestJS 模块开发流程 │
│ │
│ ① AppModule 的 imports 中植入业务模块 │
│ → @Module({ imports: [TodosModule] }) │
│ │
│ ② 每个业务模块是独立的 MVC 单元 │
│ → xx.module.ts 定义模块,组装 Controller + Service │
│ → xx.controller.ts 控制器,处理 HTTP 请求 │
│ → xx.service.ts 服务层,处理业务逻辑 │
│ │
│ ③ Service 用 @Injectable() 标记 │
│ → 自动依赖注入到 Controller │
│ → Controller 构造函数中声明依赖 │
│ → 不需要手动 new,NestJS DI 容器管理 │
│ │
│ ④ Controller 不直接操作数据库 │
│ → 通过 Service 间接操作 │
│ → MVC 分层:View(Controller) → Model(Service) │
└──────────────────────────────────────────────────────────┘
1.3 RESTful API 设计
Todos 接口设计(RESTful 风格):
HTTP 方法 路径 功能 NestJS 装饰器
──────────────────────────────────────────────────────
GET /todos 获取所有 @Get()
GET /todos/:id 获取单个 @Get(':id')
POST /todos 创建 @Post()
DELETE /todos/:id 删除 @Delete(':id')
PATCH /todos/:id 部分更新 @Patch(':id')
RESTful 核心:
├── 用 HTTP 方法区分操作类型(GET/POST/DELETE/PATCH)
├── 用 URL 路径定位资源(/todos/:id)
├── 用 HTTP 状态码表达结果(200/201/404/204)
└── 用 JSON 作为数据格式
二、根模块:AppModule 植入子模块
2.1 app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodosModule } from './todos/todos.module';
@Module({
imports: [TodosModule], // 植入 Todos 业务模块
controllers: [AppController], // 根控制器
providers: [AppService], // 根服务
})
export class AppModule {}
关键变化:
之前(单模块):
@Module({
imports: [], // 没有子模块
controllers: [AppController],
providers: [AppService],
})
现在(多模块):
@Module({
imports: [TodosModule], // ← 植入业务模块
controllers: [AppController],
providers: [AppService],
})
imports 的作用:
→ 告诉 AppModule "我依赖 TodosModule"
→ NestJS 启动时会自动加载 TodosModule
→ TodosModule 中的 Controller 和 Service 会被注册
→ 路由 /todos 会被激活
模块依赖关系图:
AppModule(根模块)
│
├── imports: [TodosModule] ← 植入
│ │
│ ├── controllers: [TodosController] → 路由 /todos
│ └── providers: [TodosService] → 业务逻辑
│
├── controllers: [AppController] → 路由 /
└── providers: [AppService] → 根服务
三、模块定义:TodosModule 的组装
3.1 todos.module.ts
import { Module } from '@nestjs/common';
import { TodosController } from './Todos.controller';
import { TodosService } from './Todos.service';
@Module({
controllers: [TodosController], // 注册控制器
providers: [TodosService], // 注册服务(可被注入)
})
export class TodosModule {}
模块的职责:
TodosModule 就是一个"装配车间":
┌──────────────────────────────────────────────┐
│ TodosModule(装配车间) │
│ │
│ controllers: [TodosController] │
│ → 注册控制器,激活 /todos 路由 │
│ │
│ providers: [TodosService] │
│ → 注册服务,放入 DI 容器 │
│ → TodosController 需要时自动注入 │
│ │
│ 模块不写业务逻辑,只负责"组装" │
└──────────────────────────────────────────────┘
MVC 分层原则:
View层(Controller)
→ 不可以直接去数据库查数据
→ 只接收请求、校验参数、调用 Service、返回响应
Model层(Service)
→ 处理业务逻辑
→ 数据库 CRUD
→ 数据处理与转换
NestJS 的 MVC:
V = Controller(视图层 = JSON 响应)
C = Controller 中的路由逻辑
M = Service + 数据库
四、控制器层:五大 HTTP 装饰器
4.1 Todos.controller.ts 完整代码
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 /todos → 获取所有
@Get()
findAll(): Todo[] {
return this.todosService.findAll();
}
// GET /todos/:id → 获取单个
@Get(':id')
findOne(@Param('id') id: string): Todo {
return this.todosService.findOne(Number(id));
}
// POST /todos → 创建
@Post()
create(@Body('title') title: string): Todo {
return this.todosService.create(title);
}
// DELETE /todos/:id → 删除
@Delete(':id')
remove(@Param('id') id: string): { message: string } {
this.todosService.remove(Number(id));
return { message: 'success' };
}
// PATCH /todos/:id → 部分更新
@Patch(':id')
update(@Param('id') id: string, @Body() patch: Partial<Todo>): Todo {
return this.todosService.update(Number(id), patch);
}
}
4.2 类装饰器:@Controller('todos')
@Controller('todos')
export class TodosController { ... }
@Controller('todos') 的作用:
→ 给控制器设置路由前缀 'todos'
→ 控制器内所有路由都自动加上 /todos 前缀
@Get() → GET /todos
@Get(':id') → GET /todos/:id
@Post() → POST /todos
@Delete(':id') → DELETE /todos/:id
@Patch(':id') → PATCH /todos/:id
没有 @Controller('todos') 的话:
@Get() → GET / ← 路径冲突
@Get(':id') → GET /:id ← 和其他控制器冲突
路由前缀让多个控制器各管各的资源,互不冲突
4.3 方法装饰器:五大 HTTP 方法
@Get() // GET → 查询资源
@Get(':id') // GET → 查询单个资源
@Post() // POST → 创建资源
@Delete(':id') // DELETE → 删除资源
@Patch(':id') // PATCH → 部分更新资源
HTTP 方法与 CRUD 的对应关系:
C(Create) → POST → 创建新资源
R(Read) → GET → 查询资源
U(Update) → PATCH → 部分更新(只改传了的字段)
→ PUT → 全量更新(替换整个资源)
D(Delete) → DELETE → 删除资源
PATCH vs PUT 的区别:
PATCH /todos/1 { "complete": true }
→ 只改 complete 字段,title 不变
PUT /todos/1 { "title": "新标题", "complete": true }
→ 整个替换,必须传所有字段
NestJS 支持的 HTTP 方法装饰器:
@Get() → GET 查询
@Post() → POST 创建
@Put() → PUT 全量更新
@Patch() → PATCH 部分更新
@Delete() → DELETE 删除
@All() → 所有方法 都匹配
@Head() → HEAD 只获取头信息
@Options() → OPTIONS 预检请求
4.4 参数装饰器:@Param 和 @Body
// @Param('id') → 从 URL 路径中提取参数
@Get(':id')
findOne(@Param('id') id: string): Todo {
return this.todosService.findOne(Number(id));
}
// 请求 GET /todos/5
// @Param('id') → id = '5'(注意:URL 参数永远是 string)
// @Body('title') → 从请求体中提取指定字段
@Post()
create(@Body('title') title: string): Todo {
return this.todosService.create(title);
}
// 请求 POST /todos
// Body: { "title": "学习 NestJS" }
// @Body('title') → title = '学习 NestJS'
// @Body() → 提取整个请求体
@Patch(':id')
update(@Param('id') id: string, @Body() patch: Partial<Todo>): Todo {
return this.todosService.update(Number(id), patch);
}
// 请求 PATCH /todos/1
// Body: { "complete": true }
// @Body() → patch = { complete: true }
NestJS 参数装饰器全家桶:
@Param('id') → URL 路径参数 /todos/:id → id
@Body('title') → 请求体指定字段 { title: 'xxx' } → title
@Body() → 整个请求体 { title, complete } → 整个对象
@Query('page') → 查询参数 /todos?page=1 → page
@Headers('auth') → 请求头指定字段 Authorization: Bearer xxx
@Req() → 整个 Request 对象
@Res() → 整个 Response 对象
参数装饰器的价值:
→ 声明式获取请求参数,不需要手动解析
→ TypeScript 类型标注,编译时检查
→ 只取需要的字段,不引入整个 Request 对象
4.5 依赖注入:构造函数注入 Service
@Controller('todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
// │ │ │
// │ │ └── 类型:TodosService
// │ │ → NestJS 根据类型从 DI 容器找实例
// │ └── readonly:只读,防止在控制器中修改 Service
// └── private:私有属性,类外部不可访问
// 注入后直接使用
@Get()
findAll(): Todo[] {
return this.todosService.findAll();
// └── 不需要手动 new TodosService()
// NestJS 自动创建并注入实例
}
}
依赖注入的完整流程:
┌──────────────────────────────────────────────────────────┐
│ 依赖注入(DI)完整流程 │
│ │
│ 1. TodosService 类被 @Injectable() 标记 │
│ → "我是一个可被注入的服务" │
│ │
│ 2. TodosModule 的 providers 注册了 TodosService │
│ → NestJS DI 容器创建并管理 TodosService 实例 │
│ │
│ 3. TodosController 构造函数声明需要 TodosService │
│ constructor(private readonly todosService: TodosService)│
│ → NestJS 看到类型是 TodosService │
│ → 从 DI 容器中取出实例 │
│ → 自动注入到构造函数参数 │
│ │
│ 4. 控制器中直接 this.todosService.findAll() │
│ → 不关心实例怎么来的,只管用 │
│ │
│ 这就是"控制反转"(IoC): │
│ 对象的创建控制权从开发者转移到了框架 │
└──────────────────────────────────────────────────────────┘
五、服务层:业务逻辑与错误处理
5.1 Todos.service.ts 完整代码
import {
Injectable,
NotFoundException,
} from '@nestjs/common';
// 数据模型接口
export interface Todo {
id: number;
title: string;
complete: boolean;
}
// 内存数据源(实际项目中替换为数据库)
let todos: Todo[] = [
{ id: 1, title: '学习 NestJS', complete: false },
{ id: 2, title: '学习 CRUD', complete: true },
];
let nextId = 3; // 自增 ID
@Injectable()
export class TodosService {
// 查询所有
findAll(): Todo[] {
return todos;
}
// 查询单个
findOne(id: number): Todo {
const todo = todos.find(t => t.id === id);
if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
return todo;
}
// 创建
create(title: string): Todo {
const todo: Todo = { id: nextId++, title, complete: 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 = todos.find(t => t.id === id);
if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
Object.assign(todo, patch);
return todo;
}
}
5.2 数据模型设计
export interface Todo {
id: number; // 唯一标识
title: string; // 任务标题
complete: boolean; // 是否完成
}
TypeScript interface 的特点:
interface Todo { ... }
→ 只描述数据结构,编译后会被完全移除
→ 不产生运行时代码
→ 用于类型检查,不占运行时体积
let todos: Todo[] = [ ... ]
let nextId = 3;
→ 用 let 而非 const:数据需要增删改
→ nextId 自增 ID 生成器
→ 实际项目中用数据库的自增 ID
5.3 五大业务方法逐个拆解
① findAll():查全部
findAll(): Todo[] {
return todos;
}
// 直接返回整个数组
// 实际项目中会加分页、过滤、排序
② findOne(id):查单个 + 错误处理
findOne(id: number): Todo {
const todo = todos.find(t => t.id === id);
if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
return todo;
}
// find 找不到返回 undefined,不是报错
// 需要手动检查并抛出 NotFoundException
// NestJS 会把 NotFoundException 转成 HTTP 404 响应
③ create(title):创建
create(title: string): Todo {
const todo: Todo = { id: nextId++, title, complete: false };
todos.push(todo);
return todo;
}
// nextId++ → 先用当前值,再自增
// 新任务默认 complete: false(未完成)
// 返回创建的 todo(包含分配的 id)
④ remove(id):删除
remove(id: number): void {
const index = todos.findIndex(t => t.id === id);
if (index === -1) throw new NotFoundException(`Todo ${id} 不存在`);
todos.splice(index, 1);
}
// findIndex 找索引,找不到返回 -1
// splice(index, 1) 从数组中删除一个元素
// 返回 void → Controller 中包装成 { message: 'success' }
⑤ update(id, patch):部分更新
update(id: number, patch: Partial<Todo>): Todo {
const todo = todos.find(t => t.id === id);
if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
Object.assign(todo, patch);
return todo;
}
// Partial<Todo> → Todo 的所有字段都变成可选
// Object.assign 把 patch 的字段合并到 todo
// 只更新传了的字段,没传的不变
5.4 Partial<T> 类型技巧
// Partial<T> 是 TypeScript 内置的工具类型
// 把接口的所有属性变成可选
interface Todo {
id: number;
title: string;
complete: boolean;
}
type PartialTodo = Partial<Todo>;
// 等价于:
// {
// id?: number;
// title?: string;
// complete?: boolean;
// }
// PATCH 请求时只传需要改的字段:
// PATCH /todos/1
// Body: { "complete": true }
// → patch = { complete: true }
// → Object.assign(todo, { complete: true })
// → 只改 complete,id 和 title 不变
Object.assign 合并原理:
const todo = { id: 1, title: '学习', complete: false };
const patch = { complete: true };
Object.assign(todo, patch);
→ { id: 1, title: '学习', complete: true }
// patch 中有的字段覆盖 todo
// patch 中没有的字段保持不变
注意:Object.assign 是浅拷贝
如果 patch 中有嵌套对象,只是引用复制
5.5 NotFoundException:标准化错误处理
import { NotFoundException } from '@nestjs/common';
findOne(id: number): Todo {
const todo = todos.find(t => t.id === id);
if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
return todo;
}
NestJS 内置错误类体系:
NotFoundException → 404 资源不存在
BadRequestException → 400 请求参数错误
UnauthorizedException → 401 未认证
ForbiddenException → 403 无权限
ConflictException → 409 冲突(如重复创建)
InternalServerErrorException → 500 服务器内部错误
throw new NotFoundException(`Todo ${id} 不存在`)
→ NestJS 拦截异常,自动转成 HTTP 响应:
{
"statusCode": 404,
"message": "Todo 5 不存在",
"error": "Not Found"
}
对比原生 Node.js:
→ 需要手动 res.status(404).json({ ... })
→ NestJS 自动处理,开发者只需 throw
→ 这就是"标准化错误输出"
错误处理的演进:
原生方式(手动处理):
if (!todo) {
res.status(404).json({ statusCode: 404, message: '不存在' });
return;
}
NestJS 方式(异常驱动):
if (!todo) throw new NotFoundException('不存在');
→ 框架自动转成 404 响应
→ 代码更简洁,关注业务逻辑而非响应格式
传统 try/catch/finally:
→ 每个方法都要写 try/catch
→ 容易遗漏,线程挂掉
→ NestJS 用异常过滤器统一拦截
→ 开发者只需 throw,框架负责兜底
六、type import:TypeScript 的导入优化
6.1 区分类型导入和值导入
// Todos.controller.ts 中的导入
import { TodosService } from './Todos.service'; // 值导入
import type { Todo } from './Todos.service'; // 类型导入
为什么要分开?
// TodosService 是一个类(运行时存在)
import { TodosService } from './Todos.service';
// → 需要在运行时创建实例、依赖注入
// → 必须值导入
// Todo 是一个接口(编译时存在,运行时消失)
import type { Todo } from './Todos.service';
// → 只用于 TypeScript 类型标注
// → 编译后会被完全移除
// → 不产生运行时代码,减少打包体积
编译前:
import { TodosService } from './Todos.service';
import type { Todo } from './Todos.service';
findAll(): Todo[] {
return this.todosService.findAll();
}
编译后(JavaScript):
import { TodosService } from './Todos.service';
// import type { Todo } → 完全消失!
findAll() {
return this.todosService.findAll();
}
// Todo[] 类型标注也消失了
6.2 type 导入的三种写法
// 写法一:独立 type import(推荐,语义最清晰)
import type { Todo } from './Todos.service';
// 写法二:内联 type 修饰符(TS 4.5+)
import { TodosService, type Todo } from './Todos.service';
// 写法三:不区分(编译器自动判断,但不推荐)
import { TodosService, Todo } from './Todos.service';
// → Todo 实际是 interface,编译器会自动移除
// → 但不够显式,可能影响 tree-shaking
七、完整请求-响应流程
7.1 端到端数据流
┌──────────────────────────────────────────────────────────────────┐
│ 完整请求-响应流程 │
│ │
│ ① 浏览器发起 HTTP 请求 │
│ GET http://localhost:3000/todos/1 │
│ │
│ ② NestJS 路由匹配 │
│ → @Controller('todos') 前缀匹配 /todos │
│ → @Get(':id') 方法匹配 /todos/1 │
│ → 提取路径参数 id = '1' │
│ │
│ ③ 参数装饰器执行 │
│ @Param('id') id: string → id = '1' │
│ → URL 参数永远是 string 类型 │
│ │
│ ④ Controller 方法执行 │
│ findOne('1') │
│ → Number('1') → 1 │
│ → this.todosService.findOne(1) │
│ │
│ ⑤ Service 业务逻辑 │
│ todos.find(t => t.id === 1) │
│ → 找到 { id: 1, title: '学习 NestJS', complete: false } │
│ → 返回 todo 对象 │
│ │
│ 如果找不到: │
│ → throw new NotFoundException('Todo 1 不存在') │
│ → NestJS 异常过滤器拦截 │
│ → 自动返回 404 响应 │
│ │
│ ⑥ Controller 返回响应 │
│ → return todo │
│ → NestJS 自动序列化为 JSON │
│ → HTTP 200 + JSON body │
│ │
│ ⑦ 浏览器收到响应 │
│ 200 OK │
│ { "id": 1, "title": "学习 NestJS", "complete": false } │
└──────────────────────────────────────────────────────────────────┘
7.2 五个接口的请求与响应
① 获取所有
GET /todos
→ 200 OK
→ [
{ "id": 1, "title": "学习 NestJS", "complete": false },
{ "id": 2, "title": "学习 CRUD", "complete": true }
]
② 获取单个
GET /todos/1
→ 200 OK
→ { "id": 1, "title": "学习 NestJS", "complete": false }
GET /todos/999
→ 404 Not Found
→ { "statusCode": 404, "message": "Todo 999 不存在", "error": "Not Found" }
③ 创建
POST /todos
Body: { "title": "学习装饰器" }
→ 201 Created
→ { "id": 3, "title": "学习装饰器", "complete": false }
④ 删除
DELETE /todos/1
→ 200 OK
→ { "message": "success" }
DELETE /todos/999
→ 404 Not Found
→ { "statusCode": 404, "message": "Todo 999 不存在", "error": "Not Found" }
⑤ 部分更新
PATCH /todos/1
Body: { "complete": true }
→ 200 OK
→ { "id": 1, "title": "学习 NestJS", "complete": true }
八、NestJS 装饰器全景图
8.1 七大核心装饰器
┌──────────────────────────────────────────────────────────────────┐
│ NestJS 七大核心装饰器 │
│ │
│ 类装饰器(修饰整个类) │
│ ├── @Controller('todos') → 设置路由前缀,标记为控制器 │
│ ├── @Module({ ... }) → 组织模块结构 │
│ └── @Injectable() → 声明服务可被依赖注入 │
│ │
│ 方法装饰器(修饰类的方法) │
│ ├── @Get() → GET 路由 │
│ ├── @Post() → POST 路由 │
│ ├── @Patch(':id') → PATCH 路由 │
│ └── @Delete(':id') → DELETE 路由 │
│ │
│ 参数装饰器(修饰方法参数) │
│ ├── @Param('id') → 从 URL 路径提取参数 │
│ └── @Body() / @Body('title') → 从请求体提取数据 │
└──────────────────────────────────────────────────────────────────┘
8.2 装饰器在各层的分布
Controller 层使用的装饰器:
@Controller('todos') → 类装饰器:路由前缀
@Get() / @Post() / ... → 方法装饰器:HTTP 路由
@Param('id') → 参数装饰器:路径参数
@Body() / @Body('title') → 参数装饰器:请求体
constructor(private readonly todosService: TodosService) → 依赖注入
Service 层使用的装饰器:
@Injectable() → 类装饰器:可注入
Module 层使用的装饰器:
@Module({ imports, controllers, providers }) → 类装饰器:模块组装
九、NestJS 分层架构总结
9.1 三层职责边界
┌──────────────────────────────────────────────────────────┐
│ NestJS 三层架构 │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Module 层(组装层) │ │
│ │ ├── @Module 装饰器 │ │
│ │ ├── imports: 子模块依赖 │ │
│ │ ├── controllers: 注册控制器 │ │
│ │ └── providers: 注册服务 │ │
│ │ 职责:组装,不写业务逻辑 │ │
│ └──────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────▼───────────────────────┐ │
│ │ Controller 层(控制层) │ │
│ │ ├── @Controller + @Get/@Post/... │ │
│ │ ├── @Param + @Body 参数提取 │ │
│ │ ├── 参数校验 │ │
│ │ ├── 调用 Service │ │
│ │ └── return 响应 │ │
│ │ 职责:路由 + 参数校验,不写业务逻辑 │ │
│ └──────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────▼───────────────────────┐ │
│ │ Service 层(业务层) │ │
│ │ ├── @Injectable 可注入 │ │
│ │ ├── 业务逻辑处理 │ │
│ │ ├── 数据 CRUD │ │
│ │ ├── 错误处理(throw NotFoundException)│ │
│ │ └── return 数据 │ │
│ │ 职责:所有业务逻辑都在这里 │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
9.2 各层"不做"什么
Module 不做:
❌ 不写业务逻辑
❌ 不处理 HTTP 请求
❌ 不操作数据库
Controller 不做:
❌ 不直接操作数据库
❌ 不写复杂业务逻辑
❌ 不做数据处理与转换
Service 不做:
❌ 不处理 HTTP 路由(不关心 URL 是什么)
❌ 不解析请求参数(参数已被 Controller 提取)
❌ 不格式化 HTTP 响应(返回纯数据,NestJS 自动序列化)
十、总结
10.1 知识体系图
NestJS CRUD 接口工程
│
├── 模块化开发流程
│ ├── AppModule imports 植入子模块
│ ├── 业务模块 = Module + Controller + Service
│ └── @Module({ controllers, providers }) 组装
│
├── Controller 层(路由 + 参数)
│ ├── @Controller('todos') 路由前缀
│ ├── @Get() @Post() @Patch() @Delete() HTTP 方法
│ ├── @Param('id') 路径参数提取
│ ├── @Body() / @Body('title') 请求体提取
│ ├── 依赖注入 constructor(Service)
│ └── 职责:路由匹配 + 参数校验 + 调用 Service
│
├── Service 层(业务 + 数据)
│ ├── @Injectable() 可注入标记
│ ├── findAll() 查全部
│ ├── findOne(id) 查单个 + NotFoundException
│ ├── create(title) 创建 + 自增 ID
│ ├── remove(id) 删除 + 错误处理
│ ├── update(id, patch) 部分更新 + Object.assign
│ └── 职责:所有业务逻辑 + 数据操作
│
├── TypeScript 技巧
│ ├── interface Todo 数据模型
│ ├── Partial<T> 所有字段变可选
│ ├── import type { Todo } 类型导入(编译后移除)
│ ├── Object.assign 浅合并
│ └── Number(id) string → number 转换
│
├── 错误处理
│ ├── NotFoundException → 404
│ ├── NestJS 内置异常类体系
│ ├── throw 异常 → 框架自动转 HTTP 响应
│ └── 标准化错误输出:statusCode + message + error
│
└── RESTful API 设计
├── GET /todos → 查全部
├── GET /todos/:id → 查单个
├── POST /todos → 创建
├── DELETE /todos/:id → 删除
└── PATCH /todos/:id → 部分更新
10.2 核心概念速查
| 概念 | 要点 |
|---|---|
| @Controller('todos') | 设置路由前缀,类内路由自动加 /todos |
| @Get / @Post / @Patch / @Delete | 方法装饰器,映射 HTTP 方法到路由 |
| @Param('id') | 从 URL 路径提取参数(永远是 string) |
| @Body() / @Body('title') | 从请求体提取数据(整个或指定字段) |
| @Injectable() | 声明 Service 可被依赖注入 |
| @Module({ controllers, providers }) | 组装模块,注册控制器和服务 |
| imports | 根模块植入子模块的配置项 |
| Partial<T> | TypeScript 工具类型,所有属性变可选 |
| import type | 类型导入,编译后完全移除,不占运行时体积 |
| NotFoundException | NestJS 内置 404 异常类,自动转 HTTP 响应 |
| Object.assign | 浅合并对象,用于 PATCH 部分更新 |
| RESTful | 用 HTTP 方法区分操作,URL 定位资源 |
10.3 一句话总结
NestJS CRUD 接口工程 = Module 组装 + Controller 路由 + Service 业务。Module 用
@Module装配,Controller 用五大 HTTP 装饰器定义路由、用@Param@Body提取参数,Service 用@Injectable标记可注入、用Partial<T>实现部分更新、用NotFoundException标准化错误。三层各司其职,装饰器贯穿始终——这就是企业级后端的 MVC 实践。
如果这篇文章对你有帮助,欢迎点赞和收藏!