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 工程化配置补全。

相关推荐
唐青枫1 小时前
一个点号省掉一堆类型:Zig .{} 语法、类型推导与实战
后端
月才1 小时前
告别System.out.println,打造专业日志系统
后端
无糖可可果1 小时前
从零理解 Docker:为什么我的代码在你的电脑上跑不起来?
javascript·后端
泡海椒1 小时前
轻量级 Java JSON 库推荐:jquick-json 序列化/反序列化 + 变量替换 + 字节码优化全解析
后端
触底反弹1 小时前
🔥 NestJS 从零到实战:一个 Todos CRUD 搞懂企业级后端框架的核心设计
后端·typescript·nestjs
吃饱了得干活1 小时前
从经典的三层架构到DDD:一次对“业务逻辑层”的解剖与重构
java·后端·架构
XuCoder1 小时前
面试官:什么是覆盖索引?
后端
林太白1 小时前
What did Trea work help me with in this optimization process
前端·后端
Asize2 小时前
3 种设计模式 + DI:Nest.js 后端第一课
后端·设计模式·nestjs