2.1 Module:工位划分 上一章把车间图纸看完了,本章开始认零件,而一切从工位画线开始。模块(Module)是 Nest.js 组织代码的第一道结构:它规定哪些零件属于同一批、哪些可以借给别的工位用。这承接 1.1 节的第三个设计决定——"边界由结构保证",模块就是那个结构本身。 模块的解剖 每个 Nest.js 应用至少有一个根模块,装饰器里四张清单定义了它的全部内容: 四张清单的语义必须咬准:controllers 和 providers 是"本模块声明并持有";imports 是"我引入哪些模块"——注意单位是模块而不是单个服务,你想用别家的服务,必须把它所在的模块整只引入,且对方还得把它列进 exports;exports 是"我愿意出借的零件"。
上一章把车间图纸看完了,本章开始认零件,而一切从工位画线开始。模块(Module)是 Nest.js 组织代码的第一道结构:它规定哪些零件属于同一批、哪些可以借给别的工位用。这承接 1.1 节的第三个设计决定——"边界由结构保证",模块就是那个结构本身。
每个 Nest.js 应用至少有一个根模块,装饰器里四张清单定义了它的全部内容:
import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { UsersRepository } from './users.repository'; @Module({ controllers: [UsersController], // 本工位的接单窗口 providers: [UsersService, UsersRepository], // 本工位的加工台 imports: [], // 要从别的工位借的零件(以模块为单位借) exports: [UsersService], // 允许借给别的工位的零件 }) export class UsersModule {}
四张清单的语义必须咬准:controllers 和 providers 是"本模块声明并持有";imports 是"我引入哪些模块"——注意单位是模块而不是单个服务,你想用别家的服务,必须把它所在的模块整只引入,且对方还得把它列进 exports;exports 是"我愿意出借的零件"。三个清单合起来构成可见性规则:不在 exports 里的 provider,对模块外彻底不可见,import 了也注入不到。
根模块负责把所有业务模块串起来:
@Module({ imports: [UsersModule, OrdersModule, InventoryModule], }) export class AppModule {}
模块划分的第一原则不是技术分层,而是业务内聚:一个业务域一个 Feature Module。用户、订单、库存各自成模块,模块内部再按"控制器—服务—仓库"分层。判据很朴素——问"这个变更会波及哪些模块":改退款规则应该只动订单模块,如果它连用户模块的文件也要碰,说明工位画线有粘连。

图里藏了一条纪律:依赖要有方向。Orders 依赖 Users 是正常的生产关系;如果反过来 Users 还要依赖 Orders,两个模块互相 import,Nest.js 会直接报循环依赖错误。这不是框架小气——互相依赖的工位通常意味着职责没切开,正确的解法是把公共部分下沉成第三个模块(2.4 节的全局模块和 3.3 节的 forwardRef 是仅有的两个例外出口,都该少用)。
纯业务模块之外,工程里总会长出一类"被多处借用的公共模块"。标准做法是建一个 SharedModule 当中转料架:它自己不装控制器,只负责把数据库、缓存这类基础模块转出口:
// 中转料架:引入基础模块并统一转出口 @Module({ imports: [TypeOrmModule.forRoot({ /* 连接配置 */ }), LoggerModule], exports: [TypeOrmModule, LoggerModule], }) export class SharedModule {}
业务模块今后只 import 这一个入口,不必逐个记住基础模块的名字。中转料架的纪律与全局公告栏(2.4 节)相反:它不省 import 这一步,省的是"该 import 谁"的记忆成本。判断某模块该进 SharedModule 还是该 @Global 的标准很简单——业务方需要"知道自己用了它"的,走料架;用了无感知、纯属基础设施的,才考虑公告栏。
从零建一个用户工位,完整走一遍(用 CLI 生成骨架,nest g module users、nest g controller users、nest g service users,工具会自动把生成的零件登记进模块清单):
// users.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class UsersService { private readonly items = [{ id: 1, name: '林工' }, { id: 2, name: '陈工' }]; findOne(id: number) { return this.items.find((u) => u.id === id) ?? null; } }
// users.controller.ts import { Controller, Get, Param, NotFoundException } from '@nestjs/common'; import { UsersService } from './users.service'; @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Get(':id') findOne(@Param('id') id: string) { const user = this.usersService.findOne(Number(id)); if (!user) throw new NotFoundException('用户不存在'); return user; } }
启动后请求 GET users/1 得到 {"id":1,"name":"林工"},请求 users/9 得到 404。注意一个细节:Controller 构造函数里注入 UsersService,但整个文件没有任何 import-for-use 之外的手动实例化——零件是容器给的。这个"给"的机制是第 3 章的全部内容,此刻你只需要确认体感:声明需要,即得供给。
新手前三周几乎必然撞上这些,提前给出症状与处置:
工位画好了,下一节进接单窗口——Controller 的路由与参数装饰器,以及"薄窗口"原则为什么值得死守。