NestJS 架构实战:给后端代码请来一位“项目经理

NestJS 架构实战:给后端代码请来一位"项目经理"

后端项目最怕的,往往不是业务复杂,而是所有东西挤在同一个文件里:路由、参数处理、数据库查询、权限判断互相缠住。功能刚开始只有几十行,几个月后却没人敢改。

NestJS 做的事很像给后端代码请来一位"项目经理":谁负责接待请求、谁负责处理业务、谁负责提供依赖,都提前安排清楚。本文用一个 Todo 模块讲明白 Module、Controller、Service、装饰器和依赖注入之间的关系。

一、NestJS 不只是"后端版 React"

NestJS 是一个基于 Node.js 和 TypeScript 的后端框架,默认可运行在 Express 上,也可切换到 Fastify。它借鉴 Angular 的模块、装饰器和依赖注入设计,但并不是把前端框架搬到服务器。

它的核心是三件事:

能力 解决的问题
Module 按业务拆分和组织代码
Decorator 用声明式语法登记路由和依赖
Dependency Injection 由容器创建并连接对象

这是一种模块化架构,不必强行叫它"严格 MVC"。一个常见的 NestJS 业务链路是 Controller 接收 HTTP 请求,Service 承载业务规则,Repository 或 ORM 访问数据。

二、应用从 NestFactory 启动

入口文件通常只有几行:

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

void bootstrap();

NestFactory.create(AppModule) 会读取根模块的元数据,建立依赖注入容器、创建 HTTP 适配器,并注册控制器中声明的路由。你不需要亲自创建 Express 实例并一条条挂路由,但底层 HTTP 服务仍然存在。

工厂模式的价值是把初始化细节藏起来,让应用从一个明确的根模块开始组装。

三、Module:业务功能的装配清单

Todo 是一个独立业务,可以有自己的模块:

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

根模块再导入它:

ts 复制代码
@Module({
  imports: [TodosModule],
})
export class AppModule {}

controllers 放对外接收请求的控制器,providers 放可由 Nest 容器管理和注入的服务。模块不是普通文件夹,而是 NestJS 用来定义可见依赖范围的边界。

当一个服务需要被其他模块使用时,要在来源模块的 exports 中导出它,再由使用方 imports 该模块。不要把所有 Provider 都做成全局可用,否则模块边界很快失去意义。

四、Controller:只负责 HTTP 世界的事

Controller 的职责是匹配路由、取请求参数、调用服务,并把结果交给框架生成响应:

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

  @Get(":id")
  findOne(@Param("id", ParseIntPipe) id: number) {
    return this.todosService.findOne(id);
  }
}

访问 GET /todos/12 时,@Controller("todos") 提供前缀,@Get(":id") 匹配路径,@Param 取出参数。ParseIntPipe 会把合法字符串转成数字,并在 abc 这类非法输入时返回 400。

不要只写 Number(id)Number("abc") 会得到 NaN,错误会拖到 Service 甚至数据库层才暴露。

Controller 不该堆复杂业务,例如"创建订单后扣库存、发消息、记审计日志"。这些属于 Service,控制器保持薄,才能复用和测试。

五、Service:业务逻辑待在这里

Service 要用 @Injectable() 标记为 Provider:

ts 复制代码
@Injectable()
export class TodosService {
  findOne(id: number) {
    const todo = this.todos.find((item) => item.id === id);
    if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
    return todo;
  }
}

NotFoundException 会被 NestJS 的异常层转换为 404 JSON 响应。它比在每个控制器里手动写 res.status(404) 更统一,也便于后续用全局异常过滤器记录日志。

上例的 this.todos 只是教程内存数组。真实项目中,Service 通常注入 Repository、Prisma Service 或其他数据访问层,而不是把数据库查询混进 Controller。

六、依赖注入:为什么不自己 new Service

构造器这一行:

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

不是 TypeScript 的魔法。NestJS 读取构造器参数类型,在当前模块及其导入模块中找到对应 Provider,创建或复用实例后注入进来。

这带来三个实际好处:

  • Controller 不需要知道 Service 如何初始化。
  • 单元测试时可以替换成 mock Service。
  • 一个 Service 依赖另一个 Service 时,依赖关系集中由容器管理。

依赖注入也不是"任何类都会被自动创建"。类必须是可见模块中的 Provider,或者通过自定义 token 明确注册;找不到依赖时,NestJS 会在启动阶段报错。

七、装饰器到底做了什么

常见装饰器的分工:

装饰器 NestJS 从中读取什么
@Module() 模块的 imports、providers、controllers
@Controller("todos") 控制器路由前缀
@Get()@Post() 方法对应的 HTTP 方法和路径
@Injectable() 类可作为 Provider 被容器管理
@Param()@Body() 方法参数从请求的什么位置提取

装饰器不是简单地"动态修改原有代码"。在 NestJS 中,它们主要把元数据附着到类、方法或参数上;框架启动时读取这些元数据,再建立路由和依赖关系。这就是为什么你写的是声明,而 NestJS 能在运行时完成注册。

八、DTO 和 Pipe:不要把"参数校验"默认交给 Controller

@Body() 只能取到请求体,本身不等于校验。创建 Todo 时可以用 DTO 描述允许的输入:

ts 复制代码
export class CreateTodoDto {
  @IsString()
  @MinLength(1)
  title: string;
}

再在入口启用全局校验:

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

这样 title 缺失、类型不对或额外字段都会在进入 Service 前被处理。Controller 负责接收和编排,Pipe 负责转换与校验,Service 才专注业务规则,这个分工比"所有逻辑都塞进方法里"更清楚。

九、面试怎么答

NestJS 用 Module 组织业务边界,用 Decorator 声明路由和 Provider 元数据,用依赖注入容器创建并连接对象。Controller 处理 HTTP 协议相关的输入输出,Service 承载业务逻辑,数据访问通常再交给 Repository 或 ORM。参数校验不是 @Body() 自动完成的,需要 DTO 配合 ValidationPipe。

总结

组件 一句话职责
AppModule 从这里开始组装整个应用
Feature Module 封装一块业务能力和依赖范围
Controller 把 HTTP 请求翻译成服务调用
Service 放业务规则和流程编排
DTO + Pipe 约束并转换外部输入
DI Container 创建、查找并注入 Provider

NestJS 的价值不在于少写几行路由,而在于让项目从第一天开始就有清晰的职责边界。下一篇就沿着这个边界,把 Todo 的增删改查和 ESLint、Prettier 工程化配置补全。

相关推荐
程序员清风8 分钟前
为什么不推荐走Agent开发?
java·后端·面试
Rain的Java大神之路43 分钟前
高并发场景下MySQL数据库性能优化实战
java·后端·mysql
明月_清风44 分钟前
分治、贪心、回溯、动态规划:四种经典算法思想
后端·算法
Johnston_man44 分钟前
golang 安装 protoc、protoc-gen-go、protoc-gen-go-grpc、protoc-gen-grpc-gateway
后端·golang
ttwuai1 小时前
Go 后台接入单点登录后,JWT、菜单权限和接口 403 怎么验?
开发语言·后端·golang
落木萧萧8251 小时前
MyBatis 启动的时候都在干什么:从 MappedStatement 说起
java·数据库·后端
国奉1 小时前
iOS 如何处理 GB 级大文件?从 FileHandle、分块读写到进度、取消与异常恢复
后端
Json____1 小时前
基于 FastAPI + Vue3 的在线拍卖系统技术解析
spring boot·后端·fastapi·wwwoop.com
创新技术阁1 小时前
FastapiAdmin 实战:二次开发前的准备(环境配置与项目启动)
前端·后端·fastapi
Java内核笔记1 小时前
Spring Boot 4.1 官方 gRPC 支持源码剖析:从社区 Starter 到一等公民
spring boot·后端