NestJS MVC 实战:模块化架构、CRUD 全流程与标准化错误处理

摘要

拆解NestJS模块化MVC架构,以Todos CRUD演示@Get/@Post/@Patch/@Delete路由、依赖注入、参数提取及NotFoundException标准化错误处理。


NestJS 将后端开发组织为一个个独立的业务模块,每个模块遵循 MVC 的三层结构:Controller 负责接收请求和参数校验,Service 负责业务逻辑和数据处理,Module 负责将前两者组装在一起。本篇以 Todos 模块的完整 CRUD 为例,展示从入口到业务层到错误处理的完整链路。

入口:工厂模式创建应用

main.ts 是 NestJS 应用的入口,使用 NestFactory 创建应用实例:

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) 是工厂模式的具体应用------NestFactory 是一个工厂类,create 方法接收根模块 AppModule,返回一个配置好的 NestJS 应用实例。开发者不需要知道工厂内部如何组装中间件、解析器、异常过滤器等底层细节,只需要告诉工厂"用哪个模块"。

app.listen(process.env.PORT ?? 3000) 启动 HTTP 服务,默认监听 3000 端口。process.env.PORT 允许通过环境变量覆盖端口号,这是部署到云平台时的标准做法。

根模块:模块的组装与依赖声明

AppModule 是应用的根模块,使用 @Module() 装饰器声明了应用的所有组成部分:

typescript 复制代码
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodosModule } from './todos/Todos.module';

@Module({
  imports: [TodosModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

@Module() 装饰器有三个核心属性:

  • imports:声明依赖的其他模块。这里导入 TodosModule,意味着 Todos 模块的控制器和服务可以在整个应用中被访问。
  • controllers:注册该模块下的控制器,负责处理 HTTP 请求。
  • providers:注册服务提供者,NestJS 的 IoC 容器会管理这些服务的实例化和注入。

imports: [TodosModule] 是关键------NestJS 通过模块导入建立依赖图,AppModule 不需要知道 TodosModule 内部有哪些控制器和服务,只需要声明"我依赖 TodosModule"。NestJS 在启动时扫描所有模块,递归解析依赖关系,构建完整的应用上下文。

TodosModule:独立业务模块

TodosModule 是一个独立的业务模块,只负责 Todo 相关的功能:

typescript 复制代码
import { Module } from '@nestjs/common';
import { TodosService } from './Todos.service';
import { TodosController } from './Todos.controller';

@Module({
  controllers: [TodosController],
  providers: [TodosService],
})
export class TodosModule {}

这个模块没有 imports,因为它不依赖其他模块。controllersproviders 各注册了一个类------TodosController 处理 HTTP 请求,TodosService 处理业务逻辑。这种"一个模块一个业务领域"的设计让代码的职责边界非常清晰。

Controller:路由与参数提取

TodosController 使用装饰器声明了五个 RESTful 接口:

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

  @Get()
  findAll(): Todo[] {
    return this.todosService.findAll();
  }

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

  @Post()
  create(@Body('title') title: string): Todo {
    return this.todosService.create(title);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() patch: Partial<Todo>): Todo {
    return this.todosService.update(Number(id), patch);
  }

  @Delete(':id')
  remove(@Param('id') id: string): { message: string } {
    this.todosService.remove(Number(id));
    return { message: '删除成功' };
  }
}

@Controller('todos') 将路径前缀设为 /todos,所有方法路由自动添加此前缀。

constructor(private readonly todosService: TodosService) {} 是依赖注入的声明。private readonly 是 TypeScript 的语法糖------声明一个私有只读属性并自动赋值。NestJS 的 IoC 容器在创建 TodosController 实例时,检测到构造函数的参数类型是 TodosService,于是从 providers 中找到对应的类,创建实例并注入。

@Get(':id') 中的 :id 是路径参数占位符。@Param('id') id: string 从请求路径中提取 id 参数的值,作为 id 变量传入方法。Number(id) 将字符串转换为数字,因为 @Param() 提取的值始终是字符串类型。

@Body('title') title: string 从请求体中提取 title 字段。@Body() patch: Partial<Todo> 获取整个请求体对象,Partial<Todo> 表示这个对象只包含 Todo 接口的部分属性。

每个方法做的事情都很薄------接收请求参数,调用 Service 对应的方法,返回结果。Controller 不包含业务逻辑,只做"分发"。

Service:业务逻辑与异常处理

TodosService 使用 @Injectable() 装饰器标记为可注入的服务:

typescript 复制代码
export interface Todo {
  id: number;
  title: string;
  complete: boolean;
}

let todos: Todo[] = [
  { id: 1, title: '学习nestjs', complete: false },
  { id: 2, title: '学习CRUD', complete: false },
];
let nextId = 3;

@Injectable()
export class TodosService {
  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;
  }

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

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

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

@Injectable() 告诉 NestJS:"这个类可以被依赖注入系统管理"。没有这个装饰器,TodosController 的构造函数中 private readonly todosService: TodosService 就无法自动注入。

数据使用内存数组 todos 存储,nextId 自增生成唯一 ID。这种设计适合原型开发和快速验证,生产环境替换为数据库查询即可。

NotFoundException:标准化错误处理

findOneremove 方法中使用了 NotFoundException

typescript 复制代码
if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);

NotFoundException 是 NestJS 内置的 HTTP 异常类,继承自 HttpException。当 throw 抛出这个异常时,NestJS 的异常过滤器会自动捕获并返回一个标准化的 JSON 响应:

json 复制代码
{
  "statusCode": 404,
  "message": "Todo 3 不存在",
  "error": "Not Found"
}

statusCode 是 HTTP 状态码 404,message 是自定义的错误描述,error 是 NestJS 自动填充的错误类型。这种标准化输出让前端和后端在错误处理上达成一致------前端可以统一解析 statusCodemessage,而不是面对各种不规则的错误格式。

手动处理的话,每一处都需要写 try/catch/finally 和自定义响应格式。NestJS 的异常类让开发者只需要关注"什么时候抛异常",而不需要关心"异常怎么格式化输出"。NestJS 还提供了其他内置异常类:

异常类 状态码 使用场景
BadRequestException 400 参数校验失败
UnauthorizedException 401 未登录
ForbiddenException 403 无权限
NotFoundException 404 资源不存在
ConflictException 409 资源冲突(如重复创建)
InternalServerErrorException 500 服务器内部错误

请求流程:从路由到响应的完整链路

一个 GET /todos/1 请求在 NestJS 中的完整处理流程:

  1. HTTP 请求到达 NestJS 内置的 HTTP 服务器
  2. NestJS 路由解析器匹配到 @Controller('todos') + @Get(':id'),路由到 TodosController.findOne
  3. @Param('id') 从 URL 中提取 id = "1"
  4. Controller 调用 this.todosService.findOne(1)
  5. Service 在 todos 数组中查找 id === 1 的元素
  6. 找到后返回 Todo 对象,Controller 直接返回,NestJS 自动序列化为 JSON
  7. 如果未找到,Service 抛出 NotFoundException
  8. NestJS 异常过滤器捕获异常,返回标准的 404 JSON 响应

这条链路体现了 MVC 的核心思想:Controller 薄(只做参数提取和路由分发),Service 厚(包含核心业务逻辑),异常标准化(统一错误格式)

总结

NestJS 的模块化 MVC 架构通过 @Module() 装饰器组装业务单元,@Controller() 处理 HTTP 请求和参数提取,@Injectable() 服务层封装业务逻辑,NotFoundException 等内置异常类实现标准化错误输出。Controller 负责"薄控制"------参数校验、调用 Service、返回结果;Service 负责"厚业务"------数据查询、业务规则、异常抛出。这种分层让每个类的职责单一、可测试、可替换。

相关推荐
浮生望2 小时前
NestJS 架构解密:从工厂模式到装饰器模式的企业级Node.js框架设计
nestjs
烬羽17 小时前
Dockerfile 是"做奶茶的配方"?用 todos 全栈项目看懂 Docker 构建与发布
nginx·docker·nestjs
Liora_Yvonne2 天前
从数据库到 Vue 页面:用 LY Fullstack 完成第一个真实全栈业务模块
前端·后端·nestjs
倾颜4 天前
NestJS 核心概念梳理:从 Module、Controller 到 Guard、Interceptor
后端·node.js·nestjs
薛定谔的算法5 天前
NestJS:让 Node.js 后端告别「野路子」
后端·node.js·nestjs
To_OC7 天前
从一行入口代码啃透 NestJS:工厂模式、装饰器和依赖注入是怎么串起来的
后端·设计模式·nestjs
__zRainy__8 天前
Node.js Web 框架选型指南:从 Express 到 Hono 的全景对比
前端·node.js·express·koa·nestjs·egg·fastify
Darling噜啦啦9 天前
NestJS 全栈 CRUD 实战:从 Module 拆分到 RESTful 七大装饰器,彻底搞懂后端接口工程
nestjs