当前位置: 首页 > news >正文

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

引言

“Nest 不是蜂巢,但它的结构比蜂巢还整齐!”
—— 一位刚学会 Nest 的开发者在深夜提交代码后如是说。


什么是 NestJS?

NestJS(简称 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文件:

PORT=1234

最后运行项目:

pnpm run start

打开浏览器访问:

  • http://localhost:1234/hello
  • http://localhost:1234/welcome
  • 或发送 POST 请求到/login(带usernamepassword

一切就绪!现在,让我们深入代码,看看 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 API)

import { 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 详解:
  1. @Get()→ GET /todos

    • 无参数。
    • 调用this.todosService.getTodos()
    • 返回 JSON 数组:
      [ {"id":1,"title":"周五狂欢","completed":false}, {"id":2,"title":"三角洲首胜","completed":true} ]
  2. @Post()→ POST /todos

    • 使用@Body('title')从请求体中提取title字段。
      • 例如,请求体为{ "title": "学习 Nest" },则title = "学习 Nest"
    • 调用this.todosService.addTodo(title)
    • 返回新创建的 Todo 对象:
      {"id":1700000000000,"title":"学习 Nest","completed":false}
  3. @Delete(':id')→ DELETE /todos/123

    • :id是路径参数占位符。
    • @Param('id', ParseIntPipe)
      • ParseIntPipe是 Nest 内置管道,自动将字符串'123'转为数字123
      • 若传入非数字(如/todos/abc),会抛出400 Bad Request错误。
    • console.log(typeof id, 'id'):调试用,确认idnumber类型。
    • 调用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导入TodosModule,Nest 会自动注册其控制器和服务。

根模块:AppModule 如何整合一切?

看看app.module.ts

import { 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(全局 Provider)

Todos 目前用内存存储,但项目已准备好 PostgreSQL 支持!

database.module.ts

import { 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中导入,其providersexports对所有模块可见,无需重复导入。
  • provide: 'PG_CONNECTION'
    • 使用字符串 Token(而非类)作为 Provider 标识。
    • useValue直接提供一个pg.Pool实例(连接池)。
  • 环境变量:从.env读取数据库配置(需用户自行设置DB_USER,DB_HOST等)。
  • exports: ['PG_CONNECTION']:允许其他模块注入此连接池。

⚠️注意.env文件应加入.gitignore,避免泄露敏感信息!


主入口:main.ts 与环境变量加载

最后看启动文件main.ts

import { 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 特性)。
    • 仅当PORTnullundefined时才用3000,若PORT=''(空字符串)则仍使用空字符串(但app.listen会报错)。
    • 更健壮的写法可能是parseInt(process.env.PORT, 10) || 3000,但当前代码已满足需求。
  • console.log:打印实际监听端口,方便调试。

数据流动全景图(以 DELETE /todos/1 为例)

让我们完整追踪一次请求的生命周期:

  1. 客户端发起请求

    DELETE http://localhost:1234/todos/1
  2. Nest 路由匹配

    • 找到TodosController(因@Controller('todos'))。
    • 匹配@Delete(':id') deleteTodo方法。
  3. 参数解析与转换

    • URL 中的'1'@Param('id', ParseIntPipe)捕获。
    • ParseIntPipe将其转为数字1
    • console.log(typeof id, 'id')输出number id
  4. 依赖注入

    • Nest 自动提供TodosService实例给TodosController
  5. 调用 Service

    • this.todosService.deleteTodo(1)被执行。
    • TodosService内部过滤掉id === 1的项。
  6. 返回响应

    • deleteTodo返回{ message: 'Todo deleted', code: 200 }
    • Nest 自动将其序列化为 JSON 并设置Content-Type: application/json
    • HTTP 状态码默认为200 OK(可通过@HttpCode()修改)。
  7. 客户端收到响应

    { "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.get<AppController>(AppController); }); describe('root', () => { it('should return greeting message in Chinese', () => { expect(appController.getHello()).toBe('你好yeah!!!'); }); }); });

详细解析

第一部分:导入依赖(Imports)

import { Test, TestingModule } from '@nestjs/testing'; import { AppController } from './app.controller'; import { AppService } from './app.service';
  • @nestjs/testing是 Nest 提供的专用测试工具包,包含:
    • Test:用于创建模拟的 Nest 应用上下文。
    • TestingModule:编译后的测试模块实例,支持.get()获取组件。
  • AppControllerAppService是被测目标及其依赖。

    关键点:测试文件只导入实际需要的类,不引入整个AppModule,体现了“最小依赖”原则。


第二部分:测试套件定义(describe('AppController', ...)

  • 使用 Jest(Nest 默认测试框架)的describe定义一个测试套件(Test Suite),聚焦于AppController类。
  • 声明appController变量用于在多个测试用例中复用,避免重复创建。

第三部分:测试前准备(beforeEach

这是整个测试的核心机制,我们拆解如下:

1.Test.createTestingModule(...)

  • 调用 Nest 的测试工厂方法,创建一个虚拟的、轻量级的 Nest 模块环境
  • 配置对象与普通@Module()几乎一致:
    • controllers: 注册待测控制器。
    • providers: 注册其依赖的服务(此处为AppService)。
  • 不包含importsexports,因为单元测试应隔离外部依赖

2..compile()

  • 异步编译测试模块,完成:
    • 依赖图构建(Dependency Graph)
    • Provider 实例化(包括单例管理)
    • 控制器与服务的依赖注入(通过构造函数自动完成)
  • 返回TestingModule实例,具备完整的 DI 容器能力。

3.app.get<AppController>(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 + 全栈学习仓库

  1. 添加updateTodo接口(PATCH /todos/:id)。
  2. 将 Todos 存入 PostgreSQL(利用已配置的PG_CONNECTION)。
  3. addTodo添加验证:标题不能为空且长度 ≥ 2。
  4. 修复单元测试中的断言错误。

你会发现,Nest 的世界,远比想象中精彩!

http://www.jsqmd.com/news/536946/

相关文章:

  • 零基础c语言入门:用快马ai快速生成你的第一个程序原型
  • ChatGLM3-6B Streamlit流式响应效果展示:逐字生成+实时思考过程可视化
  • SenseVoice-Small ONNX保姆级教程:Mac M1/M2芯片本地部署全流程
  • nli-distilroberta-base从零开始:不依赖HuggingFace Pipeline,原生PyTorch加载教程
  • 24小时自动化运行:OpenClaw+百川2-13B量化版稳定性压力测试
  • Audio Pixel Studio快速上手指南:无需命令行,浏览器内完成全部音频处理
  • LightOnOCR-2-1B快速上手:3步完成部署,开箱即用识别图片文字
  • 香港机场往返深圳包车优质品牌推荐:深圳包车直达香港、深圳包车香港包天、深圳机场包车去澳门、深圳湾直达香港包车、深圳直达中环湾仔选择指南 - 优质品牌商家
  • 5步搞定Qwen2.5-0.5B-Instruct网页推理:从下载到调用的完整教程
  • nli-distilroberta-baseGPU算力友好:兼容ROCm平台,支持AMD MI250X推理部署
  • OpenClaw低成本方案:Qwen3.5-4B-Claude模型本地化推理与Token优化
  • Sqoop NULL值处理全解析:从存储机制到生产实践
  • 检索大赛 实验4 文心4.5结果
  • langchain核心组件1-智能体
  • 不中断就能保证原子性?大错特错!
  • GTE-large多任务NLP效果惊艳展示:事件抽取与问答系统真实输出集
  • Windows系统OpenClaw完整安装部署保姆级教程(官方推荐+3种安装方式+全流程避坑指南)
  • Phi-4-Reasoning-Vision企业应用:与RAG系统集成实现文档图像知识增强推理
  • OpenClaw隐私保护方案:nanobot镜像本地化部署的3大优势
  • 漫画脸描述生成实战案例:为独立游戏开发团队生成10个NPC角色设定
  • OpenClaw插件开发入门:为Qwen3-32B镜像编写天气查询技能
  • Pixel Dream Workshop 自动化测试集成:为UI界面生成海量测试用例配图
  • PYTHON_DAY07_容器入门和字符串详解
  • ANIMATEDIFF PRO环境配置:Flask后端+HTML5前端本地调试全流程
  • PP-DocLayoutV3高算力适配:FP16推理开启后显存降低30%,精度损失<0.5%
  • 【2026 最新】Java JDK 17 安装配置详细全攻略 带图展示
  • 基于遗传算法的LQR控制器优化设计sumlink仿真模型探索
  • Keycloak 完全使用指南:从零开始理解与应用
  • STM32模拟UART实现技术详解
  • Windows系统OpenClaw安装全流程配置详解(从初始化到进阶优化,新手零踩坑)