NestJS 的模块化后端组织方式

接触 NestJS 之前,可以先把它和 Next.js 放在一起看。两者都跑在 Node 上,名字也像,但分工不同:Next.js 是 React 的全栈框架,服务端渲染、路由、API 路由都做;NestJS 则只做后端,默认用 TypeScript,把「模块化」当作组织代码的第一原则。如果 Node 后端需要一套能支撑企业级服务的结构,NestJS 是目前比较主流的选择。

后端开发在做哪些事

先想清楚后端这个位置要承担什么,再看框架怎么帮上忙。

  • 提供 API 接口,承接 Web 端请求
  • 系统集成、并发、底层服务,以及 AI Infra 这类基础设施
  • 微服务拆分

这些事共享一个需求:代码量上来之后,得有办法把不同职责拆开,否则几千行堆在一个文件里,改一处就要读懂全部。NestJS 的回答是模块化和依赖注入,下面顺着一个最小应用看它具体怎么做。

一个最小应用长什么样

安装只有两步:

css 复制代码
npm i -g @nestjs/cli
nest new hello

生成的项目,src 目录下有两个关键文件:

  • main.ts 是入口,负责启动整个应用
  • app.module.ts 是根模块,负责把各个部分组装起来

main.ts 的代码很短:

javascript 复制代码
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) 这一行值得单独看。它没有手写 new App(),而是把一个「说明书」交出去,让工厂方法按这份说明书把应用装配出来。这就是工厂模式:调用方不关心实例怎么构造,只关心拿到的是一个可用的应用。对 NestJS 来说,AppModule 就是这份说明书,里面写清楚了有哪些控制器、哪些服务、依赖哪些外部模块。

模块是独立业务单元

NestJS 的代码组织是层层递进的:

java 复制代码
App
 └─ Modules
     ├─ @nestjs/common 的 Module 类
     ├─ import 依赖项
     ├─ controller 控制器:参数校验、简单逻辑、返回 response
     └─ service 服务:返回数据

一个业务模块通常由三个文件组成,以项目里的 todos 为例:

  • todos.module.ts 负责定义和组装,声明这个模块有哪些控制器、哪些服务
  • Todos.controller.ts 是控制器,处理路由和参数
  • Todos.service.ts 是 provider,承载数据业务

todos.module.ts 只是把这两者绑在一起:

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

然后在 app.module.tsimports 里挂载这个模块,应用就多了一组 /todos 相关的接口。新增业务时,重复这个动作即可:写 service、写 controller、在 module 里组装、在 AppModule 里 import。每个模块边界清晰,不会互相侵入。

这套分工对应的是后端最常见的 MVC:Model 抽象数据库,View 是响应层,Controller 负责接收输入、校验参数、做简单逻辑,最后把数据交给 Service 返回。Controller 不直接碰数据库,Service 不直接碰 HTTP,各自守着一条边界。

装饰器给类加行为

模块化要落地,还缺一个让「说明书」生效的机制,这就是装饰器。装饰器的作用是在不修改原有对象的前提下,动态给对象附加额外功能。TypeScript 原生支持它,NestJS 把它用到了各处:

  • @Module 声明一个模块
  • @Controller('todos') 声明一个控制器,并指定路由前缀
  • @Get@Post@Patch@Delete 把类方法映射成对应的 HTTP 接口
  • @Param('id')@Body() 把请求里的参数取出来,塞进方法参数
  • @Injectable 标记一个类可以被自动注入

Todos.controller.ts 里一个接口的完整形态:

less 复制代码
@Get(':id')
findOne(@Param('id') id: string): Todo {
  return this.todosService.findOne(Number(id));
}

@Get(':id') 声明了路由,@Param('id') 声明了参数来源,方法体里只剩业务。HTTP 的解析、路由的匹配、参数的提取这些机械动作,都被装饰器接走了,Controller 不用自己写。

依赖注入让 service 自动到位

Controller 里用到 this.todosService,但这个字段并没有被手动 new 出来,它来自构造函数:

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

TodoService@Injectable() 标记后,NestJS 的 IoC 容器会在实例化 TodosController 时,自动把 TodoService 的实例塞进构造参数。Controller 不关心 service 怎么构造、依赖了谁,只需要声明「我需要一个 TodoService」。

这个注入是自动的:只要 service 加了 @Injectable(),它就能被注入到 controller,或者任何用到它的地方。依赖方向从「自己去找依赖」变成「声明依赖,由容器提供」,类之间不再直接耦合到具体实现。

错误处理走标准化的类

最后一个问题是出错时怎么办。手写 Node 服务常见 try / catch / finally 逐层包裹,但每个开发者报错格式不一致,前端拿到的东西不稳定。

NestJS 内置了一组错误类,NotFoundException 是其中之一。在 service 里,查不到数据时直接抛出:

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

框架会把这个异常转成结构化的 HTTP 响应,统一带出 statusCode 状态码和 message 消息,而不是让每个接口自己拼 JSON。业务代码里的错误处理从「捕获并格式化」简化成「抛出语义化异常」,输出格式由框架统一保证。这对一个多人协作、接口数量不断增长的工程来说,比约定俗成的 try/catch 更稳定。

收束

把上面的东西连起来,NestJS 提供的是一套固定的组织方式:入口通过工厂模式启动根模块,业务被拆成相互独立的 Module,每个 Module 内部用 Controller 接请求、用 Service 承载数据,装饰器负责把路由、参数、注入这些机械动作接走,异常则统一抛语义化错误类交给框架格式化。

这套结构不解决「后端要做什么业务」,它解决的是「业务变多之后代码怎么放、边界怎么守」。当接口从几个涨到几十个、参与的人从一个变成几个时,模块化的价值才开始显现。

相关推荐
小林ixn5 小时前
NestJS 入门实战:从 0 到 1 撸一个 Todo CRUD,感受装饰器与模块化的优雅
后端·mvc·nestjs
烬羽5 小时前
NestJS 依赖注入:Controller 里那个没有 new 的 service,到底从哪来的?
设计模式·typescript·nestjs
渣波5 小时前
NestJS 企业级后端架构实战:从核心代码到工程化思维的深度重构
前端·typescript·nestjs
不好听6135 小时前
NestJS 是什么?一张图看懂企业级后端的骨架
后端·nestjs
嘟嘟07175 小时前
NestJS 入门:从 NestFactory 入口到 Module/Controller/Service 模块化结构一次讲清
typescript·node.js·nestjs
用户9385156350717 小时前
工厂模式与 Nest.js 核心思想 —— 从蜜雪冰城到企业级架构
后端·设计模式·nestjs
用户9385156350717 小时前
实战 Todo CRUD —— 从路由到异常,手写一个完整模块
后端·typescript·nestjs
烬羽21 小时前
从 nest new 到 Hello World:一个最小 NestJS 项目,讲透工厂 + 装饰器 + 模块化
设计模式·node.js·nestjs
用户名不能为空被占用11 天前
记一次数据权限改造,看 NestJS 装饰器与守卫的组合应用
后端·nestjs