从零到精通 NestJS:深度剖析待办事项(Todos)项目,全面解析 Nest 架构、模块与数据流

从零到精通 NestJS:深度剖析待办事项(Todos)项目,全面解析 Nest 架构、模块与数据流 引言“Nest 不是蜂巢但它的结构比蜂巢还整齐”—— 一位刚学会 Nest 的开发者在深夜提交代码后如是说。什么是 NestJSNestJS简称 Nest是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的渐进式框架。它使用TypeScript编写当然也支持纯 JavaScript并深受Angular的启发——模块化、依赖注入、装饰器……这些前端熟悉的词汇在后端世界里也能大放异彩Nest 的核心思想是“结构清晰 职责分离”。它不像 Express 那样自由奔放有时甚至混乱而是像搭乐高一样把应用拆成一个个小积木Controller控制器、Service服务、Module模块各司其职井然有序。更重要的是Nest 建立在Express默认或Fastify之上这意味着你既能享受现代框架的工程化优势又不会失去底层灵活性。今天我们就通过一个超实用的待办事项Todos项目手把手带你走进 Nest 的世界我们将逐行分析每一个文件解释每个 API 的作用并完整追踪一次请求从接收到响应的全过程。第一步创建你的第一个 Nest 项目创建项目只需几步# 安装 Nest CLI全局 npm i -g nestjs/cli # 查看版本确认安装成功 nest --version # 创建新项目 nest new nest-test-demo # 进入项目目录 cd nest-test-demo # 安装依赖这里用 pnpm pnpm i # 安装 dotenv用于读取 .env 环境变量 pnpm i dotenv然后在项目根目录创建.env文件PORT1234最后运行项目pnpm run start打开浏览器访问http://localhost:1234/hellohttp://localhost:1234/welcome或发送 POST 请求到/login带username和password一切就绪现在让我们深入代码看看 Nest 是如何组织这一切的。Nest 项目的三大核心模块MVC 的现代化演进Nest 应用由三大核心构件组成它们共同构成了一个高度解耦、可测试、可维护的系统1.Module模块—— 应用的“行政区划”用Module()装饰器定义。告诉 Nest哪些 Controller、Service、其他 Module 属于这个区域。模块可以嵌套、导入、导出 Provider形成清晰的依赖树。比如AppModule是根模块TodosModule是专门管待办事项的“自治区”。2.Controller控制器—— HTTP 请求的“接待员”用Controller()装饰器定义。负责接收请求GET/POST/DELETE、解析参数、调用 Service、返回响应。控制器不应包含业务逻辑只做“协调”工作。比如AppController处理/helloTodosController处理/todos。3.Service服务—— 业务逻辑的“大脑”用Injectable()装饰器定义。封装核心逻辑操作数据、调用数据库、处理业务规则。服务是无状态的易于单元测试。比如AppService返回欢迎语TodosService管理待办列表。依赖注入DI是 Nest 的灵魂Controller 不直接创建 Service而是通过构造函数“声明需求”Nest 自动“送货上门”。这使得代码高度解耦便于替换和测试。此外Nest 还支持Middleware中间件处理请求前后的通用逻辑如日志、认证。Pipe管道验证和转换路由参数、请求体等。Guard守卫权限控制。Interceptor拦截器包装请求/响应生命周期。Exception Filter异常过滤器统一错误处理。但在本项目中我们主要聚焦于Module Controller Service这一核心三角。实战Todos 待办事项模块详解现在我们聚焦todos功能逐行解析每个文件看数据如何流动。todos.service.ts—— 业务逻辑中心纯内存实现import { Injectable } from nestjs/common export interface Todo { id: number; title: string; completed: boolean;} Injectable() export class TodosService { private todos: Todo[] [ { id: 1, title: 周五狂欢, completed: false }, { id: 2, title: 三角洲首胜, completed: true } ] getTodos() { return this.todos } addTodo(title: string) { const todo: Todo { id: Date.now(), title, completed: false } this.todos.push(todo); return todo; } deleteTodo(id: number) { this.todos this.todos.filter(todo todo.id ! id); return { message: Todo deleted, code: 200 } } }详细解析Injectable()标记该类为可被 Nest 容器管理的服务。没有它Nest 无法自动注入此服务。Todo接口定义待办事项的数据结构确保类型安全。初始数据两个硬编码的待办项用于演示。getTodos()直接返回私有数组引用⚠️ 注意实际项目应返回副本以避免外部修改。addTodo(title: string)使用Date.now()生成唯一 ID时间戳转数字。创建新Todo对象completed默认为false。推入数组并返回新对象方便前端立即使用。deleteTodo(id: number)使用filter创建新数组不可变更新。返回一个包含消息和状态码的对象模拟 RESTful 响应。⚠️重要提示此实现使用内存存储服务重启后数据丢失。生产环境应连接数据库如 PostgreSQL项目已配置。todos.controller.ts—— HTTP 接口层RESTful APIimport { Controller, Get, Post, Body, Delete, Param, ParseIntPipe, // 用于将路由参数转换为整数} from nestjs/common; import { TodosService } from ./todos.service; Controller(todos) export class TodosController{ constructor(private readonly todosService: TodosService){} Get() getTodos() { return this.todosService.getTodos(); } Post() addTodo(Body(title) title:string) { return this.todosService.addTodo(title); } Delete(:id) deleteTodo(Param(id, ParseIntPipe) id: number) { // return 111 // 打印 id 类型用于调试 console.log(typeof id,id); // 调用 todosService.deleteTodo 方法删除指定 ID 的待办事项 return this.todosService.deleteTodo(id); } }详细解析Controller(todos)所有路由以/todos为前缀。构造函数注入private readonly todosService: TodosService是 TypeScript 的简写语法等价于private todosService: TodosService; constructor(todosService: TodosService) { this.todosService todosService; }Nest 会自动提供TodosService实例。三个 API 详解Get()→ GET /todos无参数。调用this.todosService.getTodos()。返回 JSON 数组[ {id:1,title:周五狂欢,completed:false}, {id:2,title:三角洲首胜,completed:true} ]Post()→ POST /todos使用Body(title)从请求体中提取title字段。例如请求体为{ title: 学习 Nest }则title 学习 Nest。调用this.todosService.addTodo(title)。返回新创建的 Todo 对象{id:1700000000000,title:学习 Nest,completed:false}Delete(:id)→ DELETE /todos/123:id是路径参数占位符。Param(id, ParseIntPipe)ParseIntPipe是 Nest 内置管道自动将字符串123转为数字123。若传入非数字如/todos/abc会抛出400 Bad Request错误。console.log(typeof id, id)调试用确认id是number类型。调用this.todosService.deleteTodo(id)。返回删除成功消息{message:Todo deleted,code:200}✅最佳实践使用ParseIntPipe避免类型错误这是 Nest 强类型优势的体现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{}详细解析Module()装饰器配置模块元数据controllers: 声明该模块拥有的控制器。providers: 声明该模块提供的服务会被注册到 DI 容器。此模块未导出任何内容exports为空意味着其他模块只能通过导入TodosModule来间接使用其功能不能直接注入TodosService。当AppModule导入TodosModuleNest 会自动注册其控制器和服务。根模块AppModule 如何整合一切看看app.module.tsimport { Module } from nestjs/common; // 引入 Nest 模块装饰器 import { AppController } from ./app.controller; // 引入 AppController 类 import { AppService } from ./app.service; // 引入 AppService 类 import { TodosModule } from ./todos/todos.module; // 引入 TodosModule 类 import { DatabaseModule } from ./database/database.module; // 引入 DatabaseModule 类 // mvc 设计模式 模型-视图-控制器 // 一个文件一个类 // 装饰器模式 让AppModule类成为一个模块 Module({ // 定义 AppModule 类作为 Nest 应用的根模块 imports: [ TodosModule, DatabaseModule ], // 引入其他模块如果是空数组表示不引入其他模块 // controllers后端路由 控制逻辑 处理 HTTP 请求 参数校验 逻辑处理 controllers: [AppController], // 定义 AppController 类作为 Nest 应用的控制器controllers(处理 HTTP 请求) // providers 服务提供者 处理业务逻辑 数据库操作 调用其他服务 数据 providers: [AppService], // 定义 AppService 类作为 Nest 应用的服务providers(处理业务逻辑) }) export class AppModule {}详细解析imports:TodosModule引入待办事项功能。DatabaseModule引入数据库连接全局模块。controllers: 注册AppController处理根路径请求。providers: 提供AppService供AppController使用。✅模块化设计AppModule不关心TodosModule内部实现只需知道“它提供了/todos接口”完美解耦数据库模块DatabaseModule全局 ProviderTodos 目前用内存存储但项目已准备好 PostgreSQL 支持database.module.tsimport { Module, Global } from nestjs/common; // 引入 NestJS 模块和全局装饰器 // 数据库驱动 import { Pool } from pg; // 引入 pg 模块用于连接 PostgreSQL 数据库使用pnpm i pg 安装 import * as dotenv from dotenv; // 引入 dotenv 模块用于加载环境变量使用pnpm i dotenv 安装 dotenv.config(); // 加载 .env 文件中的环境变量 // 数据库基础服务 Global() // 全局服务 Module({ providers:[ { provide: PG_CONNECTION, // 连接池 useValue: new Pool({ user: process.env.DB_USER, host: process.env.DB_HOST, database: process.env.DB_NAME, password: process.env.DB_PASSWORD, port: parseInt(process.env.DB_PORT || 5432, 10), }) } ], exports: [PG_CONNECTION] }) export class DatabaseModule {}详细解析dotenv.config()在模块加载时立即读取.env文件需提前安装dotenv。Global()标记为全局模块。一旦在根模块AppModule中导入其providers和exports对所有模块可见无需重复导入。provide: PG_CONNECTION使用字符串 Token而非类作为 Provider 标识。useValue直接提供一个pg.Pool实例连接池。环境变量从.env读取数据库配置需用户自行设置DB_USER,DB_HOST等。exports: [PG_CONNECTION]允许其他模块注入此连接池。⚠️注意.env文件应加入.gitignore避免泄露敏感信息主入口main.ts 与环境变量加载最后看启动文件main.tsimport { NestFactory } from nestjs/core; // 模块化 import { AppModule } from ./app.module; import { config } from dotenv; config(); async function bootstrap() { // server app工厂模式创建 Nest 应用实例 // NestFactory 是 Nest 应用的工厂类用于创建 Nest 应用实例 // 根模型 const app await NestFactory.create(AppModule); // 3000 是默认端口也可以通过环境变量 PORT 来指定用于监听请求 // 3000 node 进程对象process.env.PORT 环境变量 PORT 的值 // 该项目内设置的环境变量 PORT 的值是 1234 console.log(process.env.PORT, process.env.PORT); // ?? 空值合并运算符来自ES2020当 process.env.PORT 为 null 或 undefined 时使用 3000 作为默认值 await app.listen(process.env.PORT ?? 3000); } bootstrap();详细解析config()再次加载.env虽然DatabaseModule已加载但这里确保主进程能读取PORT。NestFactory.create(AppModule)创建基于根模块AppModule的应用实例。process.env.PORT ?? 3000使用空值合并运算符??ES2020 特性。仅当PORT为null或undefined时才用3000若PORT空字符串则仍使用空字符串但app.listen会报错。更健壮的写法可能是parseInt(process.env.PORT, 10) || 3000但当前代码已满足需求。console.log打印实际监听端口方便调试。数据流动全景图以 DELETE /todos/1 为例让我们完整追踪一次请求的生命周期客户端发起请求DELETE http://localhost:1234/todos/1Nest 路由匹配找到TodosController因Controller(todos)。匹配Delete(:id) deleteTodo方法。参数解析与转换URL 中的1被Param(id, ParseIntPipe)捕获。ParseIntPipe将其转为数字1。console.log(typeof id, id)输出number id。依赖注入Nest 自动提供TodosService实例给TodosController。调用 Servicethis.todosService.deleteTodo(1)被执行。TodosService内部过滤掉id 1的项。返回响应deleteTodo返回{ message: Todo deleted, code: 200 }。Nest 自动将其序列化为 JSON 并设置Content-Type: application/json。HTTP 状态码默认为200 OK可通过HttpCode()修改。客户端收到响应{ message: Todo deleted, code: 200 }整个过程HTTP 请求 → Controller参数解析→ Service业务逻辑→ JSON 响应清晰无副作用深度解析NestJS 单元测试机制与AppController测试详解在 NestJS 项目中可测试性是核心设计原则之一。得益于其基于依赖注入DI和模块化架构的设计开发者可以轻松对任意组件Controller、Service、Guard 等进行隔离式单元测试而无需启动 HTTP 服务器或连接真实数据库。完整测试代码回顾import { Test, TestingModule } from nestjs/testing; import { AppController } from ./app.controller; import { AppService } from ./app.service; describe(AppController, () { let appController: AppController; beforeEach(async () { const app: TestingModule await Test.createTestingModule({ controllers: [AppController], providers: [AppService], }).compile(); appController app.getAppController(AppController); }); describe(root, () { it(should return greeting message in Chinese, () { expect(appController.getHello()).toBe(你好yeah!!!); }); }); });详细解析第一部分导入依赖Importsimport { Test, TestingModule } from nestjs/testing; import { AppController } from ./app.controller; import { AppService } from ./app.service;nestjs/testing是 Nest 提供的专用测试工具包包含Test用于创建模拟的 Nest 应用上下文。TestingModule编译后的测试模块实例支持.get()获取组件。AppController和AppService是被测目标及其依赖。✅关键点测试文件只导入实际需要的类不引入整个AppModule体现了“最小依赖”原则。第二部分测试套件定义describe(AppController, ...)使用 JestNest 默认测试框架的describe定义一个测试套件Test Suite聚焦于AppController类。声明appController变量用于在多个测试用例中复用避免重复创建。第三部分测试前准备beforeEach这是整个测试的核心机制我们拆解如下1.Test.createTestingModule(...)调用 Nest 的测试工厂方法创建一个虚拟的、轻量级的 Nest 模块环境。配置对象与普通Module()几乎一致controllers: 注册待测控制器。providers: 注册其依赖的服务此处为AppService。不包含imports、exports等因为单元测试应隔离外部依赖。2..compile()异步编译测试模块完成依赖图构建Dependency GraphProvider 实例化包括单例管理控制器与服务的依赖注入通过构造函数自动完成返回TestingModule实例具备完整的 DI 容器能力。3.app.getAppController(AppController)从测试容器中获取AppController的实例。Nest 自动完成以下操作创建AppService实例因在providers中声明调用new AppController(appService)假设构造函数为constructor(private appService: AppService)将结果赋值给appController类型断言AppController确保 TypeScript 类型安全。为什么不用new AppController(new AppService())手动创建因为真实项目中依赖可能多层嵌套如 Service A 依赖 Service B 依赖 DB手动模拟极其繁琐。Nest 的测试模块自动处理整个依赖链保持与生产环境一致的注入行为。第四部分测试用例it块describe(root, () { it(should return greeting message in Chinese, () { expect(appController.getHello()).toBe(你好yeah!!!); }); });断言逻辑appController.getHello()调用控制器方法。控制器内部通常调用this.appService.getHello()根据标准 Nest 结构。AppService.getHello()返回硬编码字符串你好yeah!!!见app.service.ts。expect(...).toBe(...)使用 Jest 的严格相等断言。 结语Nest让后端开发像搭积木一样快乐通过这个 Todos 项目我们看到了 Nest 如何用模块化 依赖注入 装饰器构建出结构清晰、易于维护的后端应用。Controller只管“接待”Service专注“干活”Module负责“划区管理”DatabaseModule提供“基础设施”。而你作为开发者只需关注业务逻辑本身剩下的交给 Nest下次当你听到“Nest 太重了”你可以微笑着说“不它只是把混乱藏起来了留给你一片整洁的代码花园。”动手试试吧克隆这个项目完成以下挑战项目源码地址lesson_zp/project/ai_fullstack/nest-test-demo/src: AI 全栈学习仓库添加updateTodo接口PATCH /todos/:id。将 Todos 存入 PostgreSQL利用已配置的PG_CONNECTION。为addTodo添加验证标题不能为空且长度 ≥ 2。修复单元测试中的断言错误。你会发现Nest 的世界远比想象中精彩