NestJS 企业级后端开发入门:从工厂模式、装饰器到模块化 CRUD
NestJS 企业级后端开发入门从工厂模式、装饰器到模块化 CRUD前言1. NestJS 解决了什么问题1.1 从“能写接口”到“能维护服务”1.2 安装、创建与运行2. 启动入口与工厂模式2.1 从 main.ts 看应用如何启动2.2 工厂模式为什么适合创建应用3. 模块化、Controller 与依赖注入3.1 根模块如何组合业务模块3.2 Controller 和 Service 各自负责什么3.3 依赖注入到底自动了什么4. 装饰器 背后的运行机制4.1 本质上是一个函数调用入口4.2 装饰器语法与装饰器设计模式的区别5. MVC 与 NestJS 分层应该怎样理解5.1 MVC 的职责划分5.2 一次请求经过哪些层6. Todo CRUD、异常处理与输入校验6.1 CRUD 路由如何映射6.2 用 Pipe、DTO 和异常建立稳定边界7. 测试、工程边界与学习路线7.1 单元测试和端到端测试7.2 从演示服务走向可用后端总结前言Node.js 能够创建 HTTP 服务但当业务从几个接口增长到用户、订单、权限、缓存、消息队列等多个领域时只靠路由回调很容易出现职责混乱、依赖失控和测试困难。NestJS 的价值不是替代 Node.js而是在 Node.js 之上提供一套结构化的服务端工程方案它以 TypeScript 为主要开发语言通过模块、控制器、Provider、装饰器和依赖注入组织应用同时可以运行在 Express 或 Fastify 等 HTTP 平台之上。需要先分清两个容易混淆的名字NestJS是面向服务端应用的 Node.js 框架Next.js是围绕 React 构建的全栈 Web 框架。前者常见Module()、Controller()和Injectable()后者常见app/page.tsx、React Server Components 与 Route Handlers。本文讨论的是NestJS 后端开发。1. NestJS 解决了什么问题1.1 从“能写接口”到“能维护服务”后端开发远不只是返回一段 JSON。一个可长期维护的服务通常需要承担 API 设计、参数校验、身份认证、业务编排、数据库访问、异常处理、日志监控、缓存、任务队列和第三方系统集成。进入更复杂的场景后还可能涉及微服务通信、实时推送以及 AI Infra 中的模型调用、向量检索与异步任务调度。Node.js 的事件循环非常适合 I/O 密集型并发例如同时等待数据库、网络和缓存响应它并不意味着单个 JavaScript 线程适合直接执行大量 CPU 密集计算。图像处理、模型推理或大规模计算通常应交给工作线程、任务队列或独立计算服务。能力直接使用 Node.js/Express 时常见做法NestJS 提供的工程抽象HTTP 路由手动注册路由回调Controller 与路由装饰器对象依赖手动new和传参IoC 容器与依赖注入业务拆分自定义目录和约定Module、Controller、Provider参数处理在回调中手动判断Pipe、DTO、ValidationPipe异常响应自己拼装状态码和 JSONHttpException 与 Exception Filter横切能力在多处重复实现Guard、Interceptor、Middleware测试手动组装依赖TestingModule 与依赖替换企业级框架的关键不在于功能“更多”而在于它能给团队提供稳定的边界、统一的约定和可替换的依赖。1.2 安装、创建与运行可以全局安装 Nest CLInpminstall-gnestjs/cli nest new hellocdhellonpmrun start:dev也可以避免全局安装使用 pnpm 临时执行pnpmdlx nestjs/cli new hellocdhellopnpmrun start:dev常用脚本如下命令用途pnpm run start普通方式启动应用pnpm run start:dev监听源码变化并自动重启pnpm run build将 TypeScript 编译到distpnpm run start:prod运行构建后的dist/mainpnpm run test运行单元测试pnpm run test:e2e运行端到端测试nest run start不是标准启动写法。开发阶段优先使用pnpm run start:dev生产启动前应先执行pnpm run build。2. 启动入口与工厂模式2.1 从 main.ts 看应用如何启动NestJS 应用从src/main.ts开始import{NestFactory}fromnestjs/core;import{AppModule}from./app.module;asyncfunctionbootstrap(){constappawaitNestFactory.create(AppModule);awaitapp.listen(process.env.PORT??3000);}bootstrap();NestFactory.create(AppModule)接收根模块创建 Nest 应用实例。这个过程会扫描模块元数据、建立 IoC 容器、解析 Provider 依赖、创建 Controller并把装饰器描述的路由注册到底层 HTTP 平台。create()返回 Promise因此启动函数使用async/await。process.env.PORT ?? 3000使用空值合并运算符环境变量不是null或undefined时使用指定端口否则监听 3000。请求进入应用后并不是“先交给 AppModule 执行业务”而是由已经建立好的路由表找到对应 ControllerAppModule 的职责是描述和组装应用。启动阶段NestJS 的主要工作载入模块读取Module()中的 imports、controllers、providers解析依赖建立 Provider 之间的依赖图创建实例按作用域实例化 Service 和 Controller注册路由读取Controller()、Get()等元数据启动监听让 Express/Fastify 接收 HTTP 请求2.2 工厂模式为什么适合创建应用工厂模式把“创建哪一种对象、如何创建对象”的细节集中到工厂中调用方只面向稳定入口。下面用饮品工厂说明interfaceDrink{show():void;}classIceCreamimplementsDrink{show(){console.log(冰激凌 3 元);}}classLemonTeaimplementsDrink{show(){console.log(柠檬水 4 元);}}typeDrinkTypeice|lemon;classMixueFactory{staticcreate(type:DrinkType):Drink{switch(type){caseice:returnnewIceCream();caselemon:returnnewLemonTea();}thrownewError(Unknown drink type);}}constdrinkMixueFactory.create(ice);drink.show();调用者只需要认识MixueFactory.create()和共同接口Drink不需要了解每种饮品的构造过程。使用字符串联合类型限制type还能避免未知类型导致undefined。NestFactory 的思路类似开发者提供根模块工厂负责选择平台适配器、创建容器并组装应用。需要注意工厂模式解决的是对象创建耦合并不等于 NestJS 的全部架构。NestJS 还综合使用了依赖注入、装饰器、模块化和面向切面等思想。3. 模块化、Controller 与依赖注入3.1 根模块如何组合业务模块根模块负责组合应用Module({imports:[TodosModule],controllers:[AppController],providers:[AppService],})exportclassAppModule{}Todo 领域再拥有独立模块Module({controllers:[TodosController],providers:[TodosService],})exportclassTodosModule{}imports用来引入其他模块controllers注册 HTTP 控制器providers注册由容器管理的服务exports则把本模块中的 Provider 暴露给其他模块。模块不是一个请求处理函数而是依赖边界和业务边界。AppModule ├── AppController ├── AppService └── TodosModule ├── TodosController └── TodosService业务扩展后可以继续拆出UsersModule、AuthModule和OrdersModule。每个模块尽量围绕一个业务领域组织而不是简单按照“所有 Controller 放一起、所有 Service 放一起”进行横向堆积。3.2 Controller 和 Service 各自负责什么Controller 处理 HTTP 协议层工作Controller(todos)exportclassTodosController{constructor(privatereadonlytodosService:TodosService){}Get(:id)findOne(Param(id)id:string):Todo{returnthis.todosService.findOne(id);}}这里Controller(todos)声明路由前缀Get(:id)声明 GET 方法和动态路径Param(id)把 URL 参数注入形参。Controller 应关注输入、校验、身份上下文和响应不应堆积复杂业务或直接书写大量 SQL。Service 承担可复用的业务逻辑Injectable()exportclassTodosService{findOne(id:number):Todo{consttodotodos.find(itemitem.idid);if(!todo){thrownewNotFoundException(Todo id not found);}returntodo;}}在真实服务中Service 通常继续调用 Repository 或 ORM 访问数据库。这样 Controller 不依赖数据库细节业务逻辑也能被 HTTP、定时任务和消息消费者复用。层次主要职责不建议承担的职责Controller接收参数、触发校验、调用 Service、组织响应复杂业务、事务、SQLService业务规则、业务编排、事务边界HTTP 请求对象和页面渲染Repository/ORM查询和持久化数据HTTP 状态码和路由Module注册并组合依赖直接处理某次请求3.3 依赖注入到底自动了什么下面的构造函数没有手动new TodosService()constructor(privatereadonlytodosService:TodosService){}这是 TypeScript 的“参数属性”语法等价于声明属性并在构造函数中赋值。Nest 在启动阶段读取构造参数的类型信息从当前模块的providers中找到TodosService创建实例后传给 Controller。TodosController 需要 TodosService ↓ 容器在 providers 中查找令牌 ↓ 创建或复用 TodosService 实例 ↓ 调用 TodosController 构造函数完成注入Injectable()表示这个类可以参与依赖注入但它并不等于“无条件自动生效”。Provider 通常还要注册在某个 Module 中跨模块使用时还需要在提供方exports并在使用方imports。默认作用域下 Provider 通常是单例因此多个请求会复用同一个 Service 实例。依赖注入的核心不是少写一个 new而是把对象创建权交给容器让业务类只声明自己需要什么。4. 装饰器 背后的运行机制4.1 本质上是一个函数调用入口不是 NestJS 独有的特殊符号而是 TypeScript 的装饰器语法。它允许一个函数作用于类、方法、属性或参数。以Controller(todos)为例可以用下面的心智模型理解constdecoratorController(todos);decorator(TodosController);Controller(todos)是装饰器工厂它先接收路由前缀再返回真正作用于类的函数。简化实现如下functionController(path:string){returnfunction(target:Function){Reflect.defineMetadata(controller:path,path,target);};}装饰器通常在模块载入阶段记录元数据NestFactory.create()随后扫描这些元数据并完成组件组装。请求到达时框架直接使用启动阶段建立的路由映射而不是每次重新执行一遍类装饰器。装饰器作用位置告诉 NestJS 什么Module()类模块包含哪些依赖和组件Controller(todos)类这是控制器前缀为/todosGet(:id)方法该方法处理 GET 动态路由Body()参数从请求体中提取值Param(id)参数从路径中提取idInjectable()类该类可以参与依赖注入TypeScript 配置中的experimentalDecorators开启传统装饰器语法emitDecoratorMetadata负责生成类型元数据。NestJS 再配合reflect-metadata才能在运行时得知 Controller 构造函数依赖哪个类。4.2 装饰器语法与装饰器设计模式的区别经典装饰器设计模式通常通过对象包装在不改变被包装对象接口的前提下叠加能力TypeScript 的Decorator则是一种语言级扩展机制可以记录元数据也可以修改属性描述符甚至替换类。两者都体现“在主体之外附加能力”的思想但不能简单画等号。NestJS 中的路由装饰器主要用于声明元数据。Guard、Interceptor 等机制才更直接地体现请求前后增强例如鉴权、日志、耗时统计和响应转换。5. MVC 与 NestJS 分层应该怎样理解5.1 MVC 的职责划分MVC 全称是 Model、View、ControllerModel管理领域数据、状态与业务规则并不只是“数据库表”的别名。View负责把结果展示给用户例如 HTML 模板或图形界面。Controller接收输入、协调 Model并选择响应形式。传统服务端 MVC 会由后端渲染 HTML。纯 API 服务通常返回 JSON没有明显的服务端 View因此 NestJS 项目更常采用 Controller、Service、Repository 分层。Module也不属于 MVC 三者之一它是 NestJS 用来组织组件和依赖的边界。经典 MVCNestJS API 中常见对应说明ModelEntity、DTO、领域对象、Repository表达数据并负责持久化协作ViewJSON 响应或独立前端API 项目通常不渲染 HTMLControllerController()类接收 HTTP 请求并调用业务层非经典 MVC 概念Service、Module业务编排与依赖组织5.2 一次请求经过哪些层访问GET /todos/1时请求链路如下客户端 ↓ GET /todos/1 Express 平台适配器 ↓ Nest 路由系统 ↓ TodosController.findOne(1) ↓ 参数转换 TodosService.findOne(1) ↓ 查询数据或抛出异常 ↓ Nest 序列化响应Controller 中的this指向当前 Controller 实例this.todosService是容器注入的属性并不指向 Module。Module 只负责建立组合关系不参与每次方法调用。6. Todo CRUD、异常处理与输入校验6.1 CRUD 路由如何映射Todo 模块已经展示了读取、删除和局部更新等操作HTTP 请求Controller 方法Service 方法语义GET /todosfindAll()findAll()查询全部任务GET /todos/:idfindOne()findOne()查询单个任务POST /todoscreate()create()创建任务PATCH /todos/:idupdate()update()局部更新任务DELETE /todos/:idremove()remove()删除任务PATCH适合只修改部分字段。PartialTodo会在类型层面把所有属性变为可选Object.assign(todo, patch)再把传入字段覆盖到目标对象update(id:number,patch:PartialTodo):Todo{consttodothis.findOne(id);Object.assign(todo,patch);returntodo;}执行顺序是先复用findOne()保证任务存在再修改对象。这里也有两个风险PartialTodo允许客户端修改id而 TypeScript 类型在运行时不会阻止恶意字段。企业应用应使用独立 DTO 和白名单校验只开放允许更新的属性。创建接口还需要真正调用 ServicePost()create(Body(title)title:string):Todo{returnthis.todosService.create(title);}Nest 对 POST 默认使用 201 状态码。仅声明空方法虽然能匹配路由却不会保存任务也不会返回创建结果。6.2 用 Pipe、DTO 和异常建立稳定边界id能把字符串转成数字但abc会得到NaN错误可能直到业务层才暴露。使用 Nest 内置ParseIntPipe可以在 Controller 边界完成转换和校验Get(:id)findOne(Param(id,ParseIntPipe)id:number):Todo{returnthis.todosService.findOne(id);}请求GET /todos/abc会直接得到 400而不会把NaN传入 Service。请求体则适合使用 DTOpnpmaddclass-validator class-transformerimport{IsNotEmpty,IsString}fromclass-validator;exportclassCreateTodoDto{IsString()IsNotEmpty()title:string;}在入口开启全局校验app.useGlobalPipes(newValidationPipe({whitelist:true,transform:true,}),);whitelist会移除 DTO 未声明的属性transform支持必要的类型转换。Controller 最终只负责接收已通过边界校验的数据Post()create(Body()dto:CreateTodoDto):Todo{returnthis.todosService.create(dto.title);}查询不到任务时Service 抛出NotFoundExceptionif(!todo){thrownewNotFoundException(Todo id not found);}Nest 的全局异常处理层会把它转换为标准 404 响应。对于可预期业务错误优先抛出合适的 HTTP 异常或领域异常不需要在每个 Controller 中重复try/catch。try/catch/finally仍然有价值try捕获当前调用链中的异常catch做恢复或转换finally释放连接等资源它不会自动捕获没有await的异步任务也不能替代统一异常边界。场景推荐处理方式路径参数格式错误Pipe例如ParseIntPipe请求体不符合规则DTO ValidationPipe资源不存在NotFoundException权限不足Guard ForbiddenException统一响应与日志Interceptor未知异常统一收口Exception Filter 与全局日志7. 测试、工程边界与学习路线7.1 单元测试和端到端测试NestJS 的TestingModule可以构造一个轻量依赖容器。单元测试直接获取 Controller 并调用方法适合验证类内部行为constappawaitTest.createTestingModule({controllers:[AppController],providers:[AppService],}).compile();constcontrollerapp.getAppController(AppController);expect(controller.getHello()).toBe(Hello World!);端到端测试则创建完整 Nest 应用再通过 Supertest 发出 HTTP 请求returnrequest(app.getHttpServer()).get(/).expect(200).expect(Hello World!);测试类型是否经过 HTTP主要验证目标特点单元测试否单个 Controller 或 Service 的行为快、定位问题清晰集成测试不一定多个 Provider 的协作覆盖模块内部组合E2E 测试是路由、校验、业务和响应整条链路最接近真实调用7.2 从演示服务走向可用后端当前 Todo 数据保存在模块级数组中适合理解 CRUD却不适合生产进程重启后数据消失多实例之间无法共享状态并发写入也缺少数据库事务保证。继续学习时可以按下面顺序演进先补齐POST /todos为 ID 使用ParseIntPipe并为请求体增加 DTO 校验。将数组替换为 Repository再接入 Prisma、TypeORM 或其他数据库访问方案。把todos和nextId收进 Service减少模块级可变状态生产环境则交给数据库生成主键。为findOne()、create()、update()和remove()编写单元测试再补齐 Todo E2E 用例。学习 ConfigModule、日志、Swagger、认证 Guard、Interceptor、缓存和任务队列。当单体模块边界稳定、确实存在独立扩缩容或团队自治需求时再考虑拆分微服务。微服务不是“更企业级”的同义词。模块化单体通常更容易开发、测试和部署只有边界、规模与运维收益足够明确时拆分才真正有价值。总结NestJS 把 Node.js 后端中反复出现的对象创建、依赖管理、路由声明和分层协作收敛为统一约定。NestFactory负责创建应用AppModule负责组合业务模块Controller 管理 HTTP 边界Service 承担业务规则Repository 负责持久化装饰器记录元数据依赖注入容器再完成实例组装。理解这些机制后Todo CRUD 就不再只是几个接口而是一条从请求校验、业务执行、异常转换到自动化测试的完整工程链路。继续扩展时应优先补齐 DTO、Pipe、数据库边界和测试再逐步学习鉴权、日志、缓存、消息队列与微服务。