实战 Todo CRUD —— 从路由到异常,手写一个完整模块

实战 Todo CRUD ------ 从路由到异常,手写一个完整模块

本篇基于第一篇的理论,通过 Todos 模块完整实现 CRUD,深入解析 @Get(':id')@Body@Put vs @PatchPartial<T>NotFoundException 等实战细节。


一、Todos 模块的文件结构

根据笔记"Module 是 nestjs 的独立业务模块",我们创建了三个文件:

  • todos.module.ts ------ 模块定义(组装)
  • todos.controller.ts ------ 控制器(路由)
  • todos.service.ts ------ 服务(数据业务)

二、模块定义 ------ todos.module.ts

typescript 复制代码
@Module({
  controllers: [TodosController],
  providers: [TodosService]
})
export class TodosModule {}

注释解读

"大型后端框架,MVC 视图层不可以直接去数据库查数据,View Controller Model"

这里说明 Nest 遵循 MVC 分层:Controller 是视图层(接收请求),Service 是模型层(处理数据)。Controller 不能直接操作数据库,必须通过 Service 层,这样保证了业务逻辑的集中和可测试性。


三、服务层 ------ Todos.service.ts 深度解析

3.1 数据模型与模拟数据库

typescript 复制代码
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;

这里用内存数组模拟数据库,nextId 负责自增 ID。这是学习阶段常用的方式,真实项目会替换为 TypeORM 等 ORM。

3.2 findAllfindOne

typescript 复制代码
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;
}

对注释的深度解读

"后端业务严谨稳定 容错模块"

这提醒我们:永远不要假设数据一定存在 。如果根据 id 找不到对应的 Todo,不能返回 nullundefined,因为调用者可能忘记判空,导致后续访问 todo.title 时报错。使用 throw 抛出 NotFoundException,Nest 会自动捕获并返回 404 状态码,这样错误处理是强制的,调用者无法忽略。

throwreturn 的区别throw立即中断函数执行,把控制权交给上层异常过滤器。这正是笔记中提到的"nest.js 提供了各种错误类,标准化错误输出,status 状态码,message 消息"。

3.3 create 方法

typescript 复制代码
create(title: string): Todo {
  const todo: Todo = {id:nextId++, title, complete: false};
  todos.push(todo);
  return todo;
}

这里使用了 nextId++ 来生成自增 ID,并默认 completefalse

3.4 remove 方法

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

注释解读

"// index"

这行注释暗示我们通过索引删除。同样,如果索引为 -1(找不到),抛异常。函数返回类型 void 表示没有返回值,但 Nest 会返回 204 状态码(如果没有额外处理)。

3.5 update 方法 ------ 理解 Partial<T>

typescript 复制代码
update(id: number, patch: Partial<Todo>): Todo {
  const todo = this.findOne(id);
  Object.assign(todo, patch);
  return todo;
}

Partial<Todo> 是什么?

它是 TypeScript 的工具类型,把 Todo 的所有属性变成可选的。也就是说,patch 可以是 { complete: true },也可以是 { title: '新标题' },甚至 { title: '新', complete: true },但不必提供所有字段。

为什么更新要用 Partial

因为前端可能只修改一个字段(比如勾选完成状态),如果要求必须传全部字段,前端就要把未修改的字段也重新传一遍,增加了网络负担,也容易出错。Partial 完美支持部分更新

Object.assign(todo, patch) 会把 patch 中的属性合并到 todo 中,未修改的字段保持不变,实现了真正的"部分更新"。


四、控制器层 ------ Todos.controller.ts 详解

4.1 路由基础

typescript 复制代码
@Controller('todos')
export class TodosController {
  constructor(private readonly todosService: TodosService) {}
  // ...
}

@Controller('todos') 指定了该控制器的基础路由 /todos,所有方法的路由都会以它开头。

4.2 GET /todos ------ 查询全部

typescript 复制代码
@Get()
findAll(): Todo[] {
  console.log('/todos controller');
  return this.todosService.findAll();
}

注释解读

"/todos" "怎么找到service? import new 实例化"

这里强调:我们并没有手动 new TodosService(),而是通过依赖注入自动获得实例。Nest 在实例化控制器时,发现构造参数需要 TodosService,就会从容器中取出已注册的实例注入进来。

4.3 GET /todos/:id ------ 查询单个

typescript 复制代码
@Get(':id')
findOne(@Param('id') id: string): Todo {
  console.log(id);
  return this.todosService.findOne(Number(id));
}
  • @Get(':id') 声明路由参数 id
  • @Param('id') 提取路径中的 id 值(字符串类型),我们通过 Number(id) 转为数字。
  • 如果 id 不存在,Service 层会抛出 NotFoundException,Nest 自动返回 404。

4.4 POST /todos ------ 创建

typescript 复制代码
@Post()
create(@Body('title') title: string): Todo {
  return this.todosService.create(title);
}
  • @Body('title') 从请求体中提取 title 字段。如果请求体是 {"title": "写文章"},则 title"写文章"

4.5 DELETE /todos/:id ------ 删除

typescript 复制代码
@Delete(':id')
remove(@Param('id') id: string): { message: string } {
  this.todosService.remove(Number(id));
  return { message: '删除成功' };
}
  • 调用 Service 的 remove 方法,如果删除成功,返回一个包含消息的对象。实际 RESTful 规范可能返回 204 无内容,但这里为了方便前端展示,返回了消息。

4.6 PUT /todos/:id ------ 更新(注意注释)

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

注释解读

"// Patch"

这里写的是 @Put,但注释却写着 Patch,这暗示了一个常见误区:PUT 应该用于全量更新,PATCH 用于部分更新 。本代码中我们使用了 Partial<Todo>,只允许部分字段,这在语义上更适合 @Patch。但为了演示灵活性,我们保留了 @Put实际开发中,请严格区分:全量用 PUT,部分用 PATCH。

@Body('') 中的空字符串表示提取整个请求体对象,赋给 patch,然后传给 Service。


五、PUT 与 PATCH 的本质区别(面试高频)

维度 PUT PATCH
语义 全量替换资源 部分更新资源
请求体 必须包含所有字段 只包含需要修改的字段
未传字段 会被覆盖为默认值或 null 保持不变
幂等性 是(多次结果一致) 不保证(如每次 +1)
RESTful 规范 用于整体更新 用于局部更新

在我们的 Todo 示例中,如果前端只传 { complete: true },使用 PUT 可能导致 title 被清空(如果服务端直接用 Object.assign 覆盖,未传字段会丢失)。但我们用了 Object.assign 合并,所以即使 PUT 也能实现部分更新,但这是违反语义的 。正确做法是改用 @Patch


六、异常处理 ------ 为什么不用 try/catch

笔记中提到:

"try catch finally ts 读秒 线程会挂"

这指的是传统的 try/catch 会阻塞事件循环(虽然 Node.js 是异步,但同步的 try/catch 会影响性能,且容易遗漏)。Nest 提供了全局异常过滤器 ,我们只需要 throw 内置的异常类(如 NotFoundException),Nest 就会自动捕获并返回标准化的错误响应,状态码和消息都是统一的。

好处

  • 业务代码不用写 try/catch,更干净。
  • 所有异常统一处理,便于日志记录和监控。
  • 返回给前端的错误格式一致,方便前端统一处理。

七、完整请求流程总结(以 GET /todos/1 为例)

  1. 浏览器发送 GET http://localhost:3000/todos/1
  2. Nest 底层 HTTP 服务器接收到请求,匹配路由。
  3. 路由发现 /todos/1 匹配到 TodosControllerfindOne 方法,且 :id 被提取为 '1'
  4. Nest 实例化 TodosController(如果尚未实例化),通过 DI 注入 TodosService
  5. 调用 findOne 方法,传入 @Param('id') 提取的 '1',转为数字。
  6. findOne 调用 this.todosService.findOne(1)
  7. Service 在 todos 数组中查找 id === 1 的项。
  8. 若找到,返回该对象;若未找到,throw new NotFoundException
  9. 若返回对象,Nest 将其序列化为 JSON 并发送 200 响应。
  10. 若抛出异常,Nest 全局过滤器捕获,生成 404 响应,并附带消息 "Todo 1 不存在"

八、总结

技术点 代码体现 底层理解
CRUD 实现了增删改查四个接口 是任何数据驱动应用的基础
路由参数 @Get(':id') + @Param('id') 提取 URL 动态段
请求体 @Body() 解析 JSON 请求体
部分更新 Partial<T> + Object.assign 只修改传入的字段
PUT vs PATCH 实际应使用 @Patch 语义决定安全性与幂等性
异常处理 throw new NotFoundException() 利用框架统一错误输出
依赖注入 constructor(private readonly service) 容器管理实例,解耦

相关推荐
GetcharZp1 小时前
Qdrant 向量数据库 Golang 实战指南:从零构建高性能检索应用
后端
hsfxuebao2 小时前
OpenSpec 完整使用流程笔记 (SDD)
后端
唐青枫2 小时前
别把 struct 只当成数据盒子:Zig 结构体从入门到实战
后端
why技术2 小时前
AI 写的文章,可能都带着手敲一遍都去不掉的“隐形水印”。
前端·人工智能·后端
北斗落凡尘3 小时前
LangGraph 入门实战(9)--中断
后端·langchain
CodeSheep3 小时前
又一个华为天才少年,离职了!
前端·后端·程序员
IT_陈寒4 小时前
Vue的双向绑定把我坑惨了,原来这个场景不能用
前端·人工智能·后端
烬羽5 小时前
从 nest new 到 Hello World:一个最小 NestJS 项目,讲透工厂 + 装饰器 + 模块化
设计模式·node.js·nestjs
SomeB1oody5 小时前
【RustyML入门】4.2. 标准化与归一化
开发语言·后端·机器学习·rust·教程