NestJS 企业级后端开发入门:从工厂模式、装饰器到模块化 CRUD

NestJS 企业级后端开发入门:从工厂模式、装饰器到模块化 CRUD

  • 前言
  • [1. NestJS 解决了什么问题](#1. NestJS 解决了什么问题)
    • [1.1 从"能写接口"到"能维护服务"](#1.1 从“能写接口”到“能维护服务”)
    • [1.2 安装、创建与运行](#1.2 安装、创建与运行)
  • [2. 启动入口与工厂模式](#2. 启动入口与工厂模式)
    • [2.1 从 main.ts 看应用如何启动](#2.1 从 main.ts 看应用如何启动)
    • [2.2 工厂模式为什么适合创建应用](#2.2 工厂模式为什么适合创建应用)
  • [3. 模块化、Controller 与依赖注入](#3. 模块化、Controller 与依赖注入)
    • [3.1 根模块如何组合业务模块](#3.1 根模块如何组合业务模块)
    • [3.2 Controller 和 Service 各自负责什么](#3.2 Controller 和 Service 各自负责什么)
    • [3.3 依赖注入到底自动了什么](#3.3 依赖注入到底自动了什么)
  • [4. 装饰器:@ 背后的运行机制](#4. 装饰器:@ 背后的运行机制)
    • [4.1 @ 本质上是一个函数调用入口](#4.1 @ 本质上是一个函数调用入口)
    • [4.2 装饰器语法与装饰器设计模式的区别](#4.2 装饰器语法与装饰器设计模式的区别)
  • [5. MVC 与 NestJS 分层应该怎样理解](#5. MVC 与 NestJS 分层应该怎样理解)
    • [5.1 MVC 的职责划分](#5.1 MVC 的职责划分)
    • [5.2 一次请求经过哪些层](#5.2 一次请求经过哪些层)
  • [6. Todo CRUD、异常处理与输入校验](#6. Todo CRUD、异常处理与输入校验)
    • [6.1 CRUD 路由如何映射](#6.1 CRUD 路由如何映射)
    • [6.2 用 Pipe、DTO 和异常建立稳定边界](#6.2 用 Pipe、DTO 和异常建立稳定边界)
  • [7. 测试、工程边界与学习路线](#7. 测试、工程边界与学习路线)
    • [7.1 单元测试和端到端测试](#7.1 单元测试和端到端测试)
    • [7.2 从演示服务走向可用后端](#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
异常响应 自己拼装状态码和 JSON HttpException 与 Exception Filter
横切能力 在多处重复实现 Guard、Interceptor、Middleware
测试 手动组装依赖 TestingModule 与依赖替换

企业级框架的关键不在于功能"更多",而在于它能给团队提供稳定的边界、统一的约定和可替换的依赖。

1.2 安装、创建与运行

可以全局安装 Nest CLI:

bash 复制代码
npm install -g @nestjs/cli
nest new hello
cd hello
npm run start:dev

也可以避免全局安装,使用 pnpm 临时执行:

bash 复制代码
pnpm dlx @nestjs/cli new hello
cd hello
pnpm run start:dev

常用脚本如下:

命令 用途
pnpm run start 普通方式启动应用
pnpm run start:dev 监听源码变化并自动重启
pnpm run build 将 TypeScript 编译到 dist
pnpm run start:prod 运行构建后的 dist/main
pnpm 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 开始:

typescript 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT ?? 3000);
}

bootstrap();

NestFactory.create(AppModule) 接收根模块,创建 Nest 应用实例。这个过程会扫描模块元数据、建立 IoC 容器、解析 Provider 依赖、创建 Controller,并把装饰器描述的路由注册到底层 HTTP 平台。create() 返回 Promise,因此启动函数使用 async/await

process.env.PORT ?? 3000 使用空值合并运算符:环境变量不是 nullundefined 时使用指定端口,否则监听 3000。请求进入应用后并不是"先交给 AppModule 执行业务",而是由已经建立好的路由表找到对应 Controller;AppModule 的职责是描述和组装应用。

启动阶段 NestJS 的主要工作
载入模块 读取 @Module() 中的 imports、controllers、providers
解析依赖 建立 Provider 之间的依赖图
创建实例 按作用域实例化 Service 和 Controller
注册路由 读取 @Controller()@Get() 等元数据
启动监听 让 Express/Fastify 接收 HTTP 请求

2.2 工厂模式为什么适合创建应用

工厂模式把"创建哪一种对象、如何创建对象"的细节集中到工厂中,调用方只面向稳定入口。下面用饮品工厂说明:

typescript 复制代码
interface Drink {
  show(): void;
}

class IceCream implements Drink {
  show() {
    console.log('冰激凌 3 元');
  }
}

class LemonTea implements Drink {
  show() {
    console.log('柠檬水 4 元');
  }
}

type DrinkType = 'ice' | 'lemon';

class MixueFactory {
  static create(type: DrinkType): Drink {
    switch (type) {
      case 'ice':
        return new IceCream();
      case 'lemon':
        return new LemonTea();
    }

    throw new Error('Unknown drink type');
  }
}

const drink = MixueFactory.create('ice');
drink.show();

调用者只需要认识 MixueFactory.create() 和共同接口 Drink,不需要了解每种饮品的构造过程。使用字符串联合类型限制 type,还能避免未知类型导致 undefined。NestFactory 的思路类似:开发者提供根模块,工厂负责选择平台适配器、创建容器并组装应用。

需要注意,工厂模式解决的是对象创建耦合,并不等于 NestJS 的全部架构。NestJS 还综合使用了依赖注入、装饰器、模块化和面向切面等思想。

3. 模块化、Controller 与依赖注入

3.1 根模块如何组合业务模块

根模块负责组合应用:

typescript 复制代码
@Module({
  imports: [TodosModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Todo 领域再拥有独立模块:

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

imports 用来引入其他模块,controllers 注册 HTTP 控制器,providers 注册由容器管理的服务,exports 则把本模块中的 Provider 暴露给其他模块。模块不是一个请求处理函数,而是依赖边界和业务边界。

text 复制代码
AppModule
├── AppController
├── AppService
└── TodosModule
    ├── TodosController
    └── TodosService

业务扩展后,可以继续拆出 UsersModuleAuthModuleOrdersModule。每个模块尽量围绕一个业务领域组织,而不是简单按照"所有 Controller 放一起、所有 Service 放一起"进行横向堆积。

3.2 Controller 和 Service 各自负责什么

Controller 处理 HTTP 协议层工作:

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

  @Get(':id')
  findOne(@Param('id') id: string): Todo {
    return this.todosService.findOne(+id);
  }
}

这里 @Controller('todos') 声明路由前缀,@Get(':id') 声明 GET 方法和动态路径,@Param('id') 把 URL 参数注入形参。Controller 应关注输入、校验、身份上下文和响应,不应堆积复杂业务或直接书写大量 SQL。

Service 承担可复用的业务逻辑:

typescript 复制代码
@Injectable()
export class TodosService {
  findOne(id: number): Todo {
    const todo = todos.find(item => item.id === id);

    if (!todo) {
      throw new NotFoundException('Todo ' + id + ' not found');
    }

    return todo;
  }
}

在真实服务中,Service 通常继续调用 Repository 或 ORM 访问数据库。这样 Controller 不依赖数据库细节,业务逻辑也能被 HTTP、定时任务和消息消费者复用。

层次 主要职责 不建议承担的职责
Controller 接收参数、触发校验、调用 Service、组织响应 复杂业务、事务、SQL
Service 业务规则、业务编排、事务边界 HTTP 请求对象和页面渲染
Repository/ORM 查询和持久化数据 HTTP 状态码和路由
Module 注册并组合依赖 直接处理某次请求

3.3 依赖注入到底自动了什么

下面的构造函数没有手动 new TodosService()

typescript 复制代码
constructor(private readonly todosService: TodosService) {}

这是 TypeScript 的"参数属性"语法,等价于声明属性并在构造函数中赋值。Nest 在启动阶段读取构造参数的类型信息,从当前模块的 providers 中找到 TodosService,创建实例后传给 Controller。

text 复制代码
TodosController 需要 TodosService
              ↓
容器在 providers 中查找令牌
              ↓
创建或复用 TodosService 实例
              ↓
调用 TodosController 构造函数完成注入

@Injectable() 表示这个类可以参与依赖注入,但它并不等于"无条件自动生效"。Provider 通常还要注册在某个 Module 中;跨模块使用时,还需要在提供方 exports,并在使用方 imports。默认作用域下 Provider 通常是单例,因此多个请求会复用同一个 Service 实例。

依赖注入的核心不是少写一个 new,而是把对象创建权交给容器,让业务类只声明自己需要什么。

4. 装饰器:@ 背后的运行机制

4.1 @ 本质上是一个函数调用入口

@ 不是 NestJS 独有的特殊符号,而是 TypeScript 的装饰器语法。它允许一个函数作用于类、方法、属性或参数。以 @Controller('todos') 为例,可以用下面的心智模型理解:

typescript 复制代码
const decorator = Controller('todos');
decorator(TodosController);

Controller('todos') 是装饰器工厂,它先接收路由前缀,再返回真正作用于类的函数。简化实现如下:

typescript 复制代码
function Controller(path: string) {
  return function (target: Function) {
    Reflect.defineMetadata('controller:path', path, target);
  };
}

装饰器通常在模块载入阶段记录元数据;NestFactory.create() 随后扫描这些元数据并完成组件组装。请求到达时,框架直接使用启动阶段建立的路由映射,而不是每次重新执行一遍类装饰器。

装饰器 作用位置 告诉 NestJS 什么
@Module() 模块包含哪些依赖和组件
@Controller('todos') 这是控制器,前缀为 /todos
@Get(':id') 方法 该方法处理 GET 动态路由
@Body() 参数 从请求体中提取值
@Param('id') 参数 从路径中提取 id
@Injectable() 该类可以参与依赖注入

TypeScript 配置中的 experimentalDecorators 开启传统装饰器语法,emitDecoratorMetadata 负责生成类型元数据。NestJS 再配合 reflect-metadata,才能在运行时得知 Controller 构造函数依赖哪个类。

4.2 装饰器语法与装饰器设计模式的区别

经典装饰器设计模式通常通过对象包装,在不改变被包装对象接口的前提下叠加能力;TypeScript 的 @Decorator 则是一种语言级扩展机制,可以记录元数据,也可以修改属性描述符甚至替换类。两者都体现"在主体之外附加能力"的思想,但不能简单画等号。

NestJS 中的路由装饰器主要用于声明元数据。Guard、Interceptor 等机制才更直接地体现请求前后增强,例如鉴权、日志、耗时统计和响应转换。

5. MVC 与 NestJS 分层应该怎样理解

5.1 MVC 的职责划分

MVC 全称是 Model、View、Controller:

  • Model 管理领域数据、状态与业务规则,并不只是"数据库表"的别名。
  • View 负责把结果展示给用户,例如 HTML 模板或图形界面。
  • Controller 接收输入、协调 Model,并选择响应形式。

传统服务端 MVC 会由后端渲染 HTML。纯 API 服务通常返回 JSON,没有明显的服务端 View,因此 NestJS 项目更常采用 Controller、Service、Repository 分层。Module 也不属于 MVC 三者之一,它是 NestJS 用来组织组件和依赖的边界。

经典 MVC NestJS API 中常见对应 说明
Model Entity、DTO、领域对象、Repository 表达数据并负责持久化协作
View JSON 响应或独立前端 API 项目通常不渲染 HTML
Controller @Controller() 接收 HTTP 请求并调用业务层
非经典 MVC 概念 Service、Module 业务编排与依赖组织

5.2 一次请求经过哪些层

访问 GET /todos/1 时,请求链路如下:

text 复制代码
客户端
  ↓ 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 /todos findAll() findAll() 查询全部任务
GET /todos/:id findOne() findOne() 查询单个任务
POST /todos create() create() 创建任务
PATCH /todos/:id update() update() 局部更新任务
DELETE /todos/:id remove() remove() 删除任务

PATCH 适合只修改部分字段。Partial<Todo> 会在类型层面把所有属性变为可选,Object.assign(todo, patch) 再把传入字段覆盖到目标对象:

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

执行顺序是先复用 findOne() 保证任务存在,再修改对象。这里也有两个风险:Partial<Todo> 允许客户端修改 id,而 TypeScript 类型在运行时不会阻止恶意字段。企业应用应使用独立 DTO 和白名单校验,只开放允许更新的属性。

创建接口还需要真正调用 Service:

typescript 复制代码
@Post()
create(@Body('title') title: string): Todo {
  return this.todosService.create(title);
}

Nest 对 POST 默认使用 201 状态码。仅声明空方法虽然能匹配路由,却不会保存任务,也不会返回创建结果。

6.2 用 Pipe、DTO 和异常建立稳定边界

+id 能把字符串转成数字,但 +'abc' 会得到 NaN,错误可能直到业务层才暴露。使用 Nest 内置 ParseIntPipe 可以在 Controller 边界完成转换和校验:

typescript 复制代码
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number): Todo {
  return this.todosService.findOne(id);
}

请求 GET /todos/abc 会直接得到 400,而不会把 NaN 传入 Service。请求体则适合使用 DTO:

bash 复制代码
pnpm add class-validator class-transformer
typescript 复制代码
import { IsNotEmpty, IsString } from 'class-validator';

export class CreateTodoDto {
  @IsString()
  @IsNotEmpty()
  title: string;
}

在入口开启全局校验:

typescript 复制代码
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
    transform: true,
  }),
);

whitelist 会移除 DTO 未声明的属性,transform 支持必要的类型转换。Controller 最终只负责接收已通过边界校验的数据:

typescript 复制代码
@Post()
create(@Body() dto: CreateTodoDto): Todo {
  return this.todosService.create(dto.title);
}

查询不到任务时,Service 抛出 NotFoundException

typescript 复制代码
if (!todo) {
  throw new NotFoundException('Todo ' + id + ' not found');
}

Nest 的全局异常处理层会把它转换为标准 404 响应。对于可预期业务错误,优先抛出合适的 HTTP 异常或领域异常,不需要在每个 Controller 中重复 try/catchtry/catch/finally 仍然有价值:try 捕获当前调用链中的异常,catch 做恢复或转换,finally 释放连接等资源;它不会自动捕获没有 await 的异步任务,也不能替代统一异常边界。

场景 推荐处理方式
路径参数格式错误 Pipe,例如 ParseIntPipe
请求体不符合规则 DTO + ValidationPipe
资源不存在 NotFoundException
权限不足 Guard + ForbiddenException
统一响应与日志 Interceptor
未知异常统一收口 Exception Filter 与全局日志

7. 测试、工程边界与学习路线

7.1 单元测试和端到端测试

NestJS 的 TestingModule 可以构造一个轻量依赖容器。单元测试直接获取 Controller 并调用方法,适合验证类内部行为:

typescript 复制代码
const app = await Test.createTestingModule({
  controllers: [AppController],
  providers: [AppService],
}).compile();

const controller = app.get<AppController>(AppController);
expect(controller.getHello()).toBe('Hello World!');

端到端测试则创建完整 Nest 应用,再通过 Supertest 发出 HTTP 请求:

typescript 复制代码
return request(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 或其他数据库访问方案。
  • todosnextId 收进 Service,减少模块级可变状态;生产环境则交给数据库生成主键。
  • findOne()create()update()remove() 编写单元测试,再补齐 Todo E2E 用例。
  • 学习 ConfigModule、日志、Swagger、认证 Guard、Interceptor、缓存和任务队列。
  • 当单体模块边界稳定、确实存在独立扩缩容或团队自治需求时,再考虑拆分微服务。

微服务不是"更企业级"的同义词。模块化单体通常更容易开发、测试和部署;只有边界、规模与运维收益足够明确时,拆分才真正有价值。

总结

NestJS 把 Node.js 后端中反复出现的对象创建、依赖管理、路由声明和分层协作收敛为统一约定。NestFactory 负责创建应用,AppModule 负责组合业务模块,Controller 管理 HTTP 边界,Service 承担业务规则,Repository 负责持久化;装饰器记录元数据,依赖注入容器再完成实例组装。理解这些机制后,Todo CRUD 就不再只是几个接口,而是一条从请求校验、业务执行、异常转换到自动化测试的完整工程链路。继续扩展时,应优先补齐 DTO、Pipe、数据库边界和测试,再逐步学习鉴权、日志、缓存、消息队列与微服务。

相关推荐
熊猫钓鱼>_>7 个月前
【开源鸿蒙跨平台开发先锋训练营】Day 8:鸿蒙 Next + React Native 实战:打造丝滑的四Tab底部导航体验
react native·开源·list·tab·harmonyos·鸿蒙·next
zhujian826377 个月前
三十、【鸿蒙 NEXT】实现吸顶效果
harmonyos·鸿蒙·next·吸顶·吸顶效果·nestedscroll
wx_lidysun7 个月前
Nextjs学习笔记
前端·react·next
彩旗工作室8 个月前
Clerk 完全指南:现代 Web 应用的用户认证革命
react·next·用户认证·clerk
至善迎风9 个月前
React2Shell(CVE-2025-55182)漏洞服务器排查完整指南
网络安全·react·数据安全·漏洞·next·rsc·cve-2025-55182
患得患失94910 个月前
【NextJS】NextJS后端框架:class-validator vs zod
next
溪饱鱼1 年前
第十章:Next的Seo实践
javascript·html·next·dreamweaver
代码搬运媛1 年前
Next.js路由导航完全指南
前端·javascript·vue.js·next
孤蓬&听雨1 年前
前端开发10大框架深度解析
前端·vue·框架·react·开发·nuxt·next