Skip to content

NestJS 模块

目录

模块

模块是一个用 @Module() 装饰器标注的类。这个装饰器提供了 Nest 用于组织和管理应用结构的元数据,用于聚合相关的 controller、service、provider 等组件

@Module() 并不执行任何逻辑,它只是告诉 Nest “这个类是个模块,模块里有谁”。

每个 Nest 应用至少有一个模块,即根模块(Root Module),它是 Nest 构建“应用依赖图”的起点。Nest 内部会基于这个模块图来解析模块之间的依赖关系。

应用的 AppModule 就是根模块(main.ts 中会传给 NestFactory.create(AppModule));

Nest 会从这里出发,自动“扫描”其他模块、控制器、服务,构建出完整的“依赖图”

虽然小型应用可能只有一个模块,但 Nest 强烈建议按功能拆分多个模块,每个模块封装一组紧密相关的功能。

ts
@Module({
  imports: [OtherModule],
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
字段作用实战说明
providers注册服务/值/工厂等提供器,供模块内部使用类似依赖注入容器的“注册表”
controllers控制器列表,Nest 会自动创建并路由这些控制器只能放控制器类
imports导入其他模块,以便使用它们导出的 provider实现模块之间通信和依赖
exports把本模块中某些 provider 显式暴露出去,供其他模块使用类似“公共接口”或模块的 API

模块默认是封闭的(encapsulated),你无法直接使用其他模块中的服务,除非它们被导出(export)了。

错误用法

ts
@Module({
  providers: [AuthService],
})
export class AuthModule {}
ts
@Module({
  imports: [AuthModule],
})
export class UserModule {
  constructor(private authService: AuthService) {} // 注入失败!AuthService 没有被导出
}

正确用法

ts
@Module({
  providers: [AuthService],
  exports: [AuthService], // 显式导出
})
export class AuthModule {}

功能模块

功能模块就是把某一“业务域”相关的 controller、service、DTO、entity、interface 等封装进一个独立模块

它体现了 单一职责原则(SOLID 原则中的 S),每个模块只负责自己的功能,耦合更低、可维护性强。

示例:

创建一个CatsModule

ts
// cats/cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

该模块聚合了 CatsController + CatsService

它没有导出任何 provider,因为目前只在模块内部使用;

未来如果其他模块需要用 CatsService,再通过 exports: [CatsService] 显式导出。

提示:用 CLI 快速生成模块

bash
nest g module cats

这条命令自动创建了 cats/cats.module.ts,推荐用 CLI 快速生成基础结构,保持规范一致。

接入根模块(AppModule)

ts
// app.module.ts
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}

你必须在根模块中 imports 引入你的功能模块,Nest 才能“识别”并注册它;

一旦引入,CatsModule 内的 controller 和 provider 就会被自动扫描并初始化;

Nest 从 AppModule 出发构建模块依赖图,不 import 就不生效

最终目录结构示意

ts
src/
├── cats/
│   ├── dto/
│   │   └── create-cat.dto.ts
│   ├── interfaces/
│   │   └── cat.interface.ts
│   ├── cats.controller.ts
│   ├── cats.module.ts
│   └── cats.service.ts
├── app.module.ts
└── main.ts

共享模块

在 Nest 中,默认情况下模块是单例,因此你可以轻松地在多个模块之间共享任何提供程序的同一实例。

默认应用中只创建一个实例,整个生命周期共用,所以,只要模块正确导出,其他模块可以共享这个服务的同一个实例,而不是重复创建;

这也正是依赖注入的一个核心优势:统一生命周期管理 + 状态共享 + 内存复用

示例:导出共享服务

ts
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService], // 导出 CatsService
})
export class CatsModule {}

NestJS 模块是封闭的,不导出 = 其他模块看不到,只有被 exports 的 provider,才可以被 import 此模块的其他模块访问到,想象成模块是一个类,exports 就是它的对外公共 API。

示例:使用共享服务

ts
// cats.module.ts 定义 + 导出
@Module({
  providers: [CatsService],
  exports: [CatsService], // 关键!导出,否则其他
})
export class CatsModule {}
ts
// app.module.ts 导入 CatsModule
@Module({
  providers: [AppService],
  imports: [CatsModule],
})
export class AppModule {}
ts
// app.service.ts 注入共享服务
@Injectable()
export class AppService {
  constructor(private readonly catsService: CatsService) {
    // 此处注入成功,因为 CatsService 是从 CatsModule 导出的
  }
}

如果我们在每个模块中都重新注册 CatsService,每个模块会得到一个新的实例,造成:

  • 更多内存开销(多个实例驻留内存);
  • 共享状态不一致(例如缓存、计数器、配置状态);
  • 行为变得不可预测(你以为是单例,其实不是);

所以除非你明确需要服务是“请求级”、“瞬态”、“隔离的”,否则共享单例是推荐做法。

共享模块的两个关键条件

条件解释
1️⃣ 被导出(exports: [Service]告诉 Nest 哪些服务可供外部模块使用
2️⃣ 被导入(imports: [SharedModule]其他模块必须显式引入该模块才能使用其中导出的服务

注意

ts
@Module({
  providers: [CatsService], // 在多个模块都这样写
})
export class DogsModule {}

这样会让 DogsModule 拿到的是自己的 CatsService 实例,和其他模块的不一致,因为此处又重新注册了一个 CatsService 实例。

共享模块 = 导出可复用的服务,供多个模块共享一个实例使用,这是 NestJS 的依赖注入和模块系统的最大优势之一。

模块重新导出

如上所示,模块可以导出其内部提供程序。此外,它们还可以重新导出它们所导入的模块

在下面的示例中,CommonModule 被导入到 CoreModule 中,然后又从 CoreModule 导出,使得导入 CoreModule 的其他模块也能访问 CommonModule 的导出内容

ts
@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

重新导出可以理解为在模块之间建立「服务转发通道」:A 模块导入了 B 模块,并在自己的 exports 中再次导出 B 模块,那今后任何导入 A 模块的模块,也可以访问 B 模块导出的 provider,就像直接导入了 B 一样。

这就是所谓的 re-export(再导出)

这样做通常是为了 模块解耦、逻辑清晰,假设你有一个通用模块 CommonModule,里面封装了全局日志服务、异常处理器、转换管道等 ,所有模块都要用:

ts
@Module({
  providers: [LoggerService, TransformPipe],
  exports: [LoggerService, TransformPipe],
})
export class CommonModule {}

现在你又有个中间模块 CoreModule,它希望把所有「基础依赖」统一打包、暴露出去:

ts
@Module({
  imports: [CommonModule],
  exports: [CommonModule], // 再导出 CommonModule
})
export class CoreModule {}

这样你就可以在别的模块里只导入 CoreModule,而不再关心里面用了哪些 Common 组件:

ts
@Module({
  imports: [CoreModule], // 一次性拿到所有底层依赖
})
export class FeatureModule {}

实战建议

ts
// common.module.ts
@Module({
  providers: [LoggerService, ExceptionFilter],
  exports: [LoggerService, ExceptionFilter],
})
export class CommonModule {}
ts
// core.module.ts
@Module({
  imports: [CommonModule],
  exports: [CommonModule], // 再导出
})
export class CoreModule {}
ts
// users.module.ts
@Module({
  imports: [CoreModule], // 需要知道 CommonModule 存在
})
export class UsersModule {}

✅ 好处:使用者只依赖 CoreModule,不关心它内部封装了什么模块; ✅ 模块边界清晰,团队协作更流畅,结构也更稳固。

模块“重新导出”是构建稳定依赖体系的关键工具,让你可以隐藏实现细节、合并多个通用模块,并通过一个出口统一暴露依赖,尤其适合构建「核心模块」「平台模块」「中间件模块」等公共中转结构。

模块重新导出(Re-export)的本质:就是把一堆零散的服务(或模块)封装打包后,通过一个模块统一暴露出去,方便批量引入 & 隐藏细节

场景对应关系表

想做的事常规做法(麻烦)推荐做法(re-export)
引入多个基础服务在每个模块中 imports: [CommonModule, LoggerModule, ConfigModule]CoreModule 中统一导入再导出它们,然后只引入 CoreModule
暴露一组内部 Service在每个功能模块里分别导出这些服务把它们集中放在一个 SharedModule 或 CoreModule,统一 export
ts
// 你项目中有多个底层模块:

[ConfigModule]
[LoggerModule]
[ExceptionModule]

// 在 CoreModule 中统一 re-export:

@Module({
  imports: [ConfigModule, LoggerModule, ExceptionModule],
  exports: [ConfigModule, LoggerModule, ExceptionModule],
})
export class CoreModule {}

// 别的模块只 import CoreModule,就能使用它们全部的 Service:
ts
@Module({
  imports: [CoreModule], // 一次搞定全部依赖
})
export class UsersModule {}

Re-export 让你可以只 import 一个模块,就像 import 了多个服务/模块一样,不仅省事,还能封装依赖、保持结构整洁,是大型 Nest 应用架构的“标配手段”。

依赖注入

模块类(@Module()装饰的类)能否使用依赖注入(DI)

模块类也可以注入提供程序(例如,出于配置目的):

ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {
  constructor(private catsService: CatsService) {}
}

表面上看,这是把 CatsService 注入进了 CatsModule,但实际上,这并不是一个有效的 Nest 注入行为

由于循环依赖,模块类本身不能作为提供器注入

NestJS 的模块类(如 CatsModule):

  • 不会自动成为 provider
  • 不会被 Nest 注入器处理(Nest 不会去调用它的 constructor 并解析依赖);
  • 所以你在 constructor() 里声明任何依赖项,也不会真的注入任何东西,也不会报错,但你拿到的会是 undefined

为什么模块类不能注入依赖

从设计上讲,模块类只是一个“配置容器”,是用来告诉 Nest 这个模块有哪些 provider、controller、import/export 的。

它并不是运行时参与逻辑的实例,所以 Nest 根本就不会去「实例化模块类并注入依赖」,否则容易出现下面的问题:

经典的循环依赖场景

  • 你在模块里注入了某个服务;
  • 而该服务又在自己的构造函数中依赖这个模块(或其导出的其他服务);
  • 那么就形成循环依赖:模块 → 服务 → 模块。

Nest 解决这类问题的策略就是——模块类不参与依赖注入,彻底切断这条循环链。

那么什么时候需要在类中注入服务

类类型是否允许注入依赖?说明
@Injectable() 的类(比如 Service)✅ 是Nest 会自动构造并注入
@Controller() 的类✅ 是控制器也被 Nest 管理
@Pipe()@Guard() 等 provider✅ 是都可以注入依赖
@Module() 的类(模块类)❌ 否它不是 provider,不参与注入

如果你真的需要“模块初始化行为”怎么办

1.使用生命周期钩子 onModuleInit() 推荐

ts
@Injectable()
export class CatsService implements OnModuleInit {
  onModuleInit() {
    console.log('CatsService 已初始化完成');
  }
}

这种方式只适用于 provider,不适用于模块类。

2.使用自定义 provider + 工厂函数(更灵活)

ts
@Module({
  providers: [
    {
      provide: 'CONFIG_INITIALIZER',
      useFactory: (catsService: CatsService) => {
        catsService.setupSomething();
      },
      inject: [CatsService],
    },
  ],
})
export class CatsModule {}

这种方式你可以在模块内部执行初始化逻辑,但逻辑发生在 provider 初始化阶段。

模块类本身不参与依赖注入,它只是一个元数据容器。如果你需要执行初始化逻辑,应该写在 provider(service)中,通过生命周期钩子或工厂函数来实现。

生命周期钩子的两种写法

全局模块

如果你必须在所有地方导入相同的模块集合,这种重复操作会变得乏味。

Nest 将提供程序封装在模块作用域内,只有在导入了该模块的情况下,其他模块才能使用其提供的服务。

当你想要提供一组“开箱即用”的提供程序(例如辅助工具、数据库连接等)时,可以使用 @Global() 装饰器将模块设置为全局模块。

ts
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

@Global() 装饰器让该模块变成全局模块。 全局模块应该只注册一次,通常由根模块或核心模块注册。 在上面的例子中,CatsService 将变成应用中的全局单例,任何模块都可以注入它而无需在自己的 imports 数组中导入 CatsModule

全局模块的意义

  • 避免在每个需要的模块中都重复导入相同的基础模块(例如数据库模块、配置模块、日志模块等)。
  • 使核心服务变成全局可用,简化模块依赖配置。

设计建议

  • 不建议所有模块都全局化,否则模块间依赖关系混乱、耦合度高,难以维护。
  • 推荐仅将那些真正需要全局共享的核心服务设为全局模块。
  • 对于其他服务,仍然应通过模块显式导入保持清晰的边界和结构。

实际使用场景示例

ts
@Global()
@Module({
  providers: [DatabaseService],
  exports: [DatabaseService],
})
export class DatabaseModule {}
ts
@Module({
  imports: [DatabaseModule], // 只在根模块或核心模块导入一次
})
export class AppModule {}

这样,DatabaseService 在应用所有其他模块中都是可用的,无需重复导入 DatabaseModule

只需在应用的根模块或核心模块导入一次全局模块即可。

其他模块不需要导入,但仍可通过依赖注入使用全局服务。

如果你重复导入了全局模块,不会创建新的实例,依然是单例。

@Global() 装饰器能将模块标记为全局模块,使其导出的服务自动对整个应用可见,无需重复导入。 这极大减少了重复导入的样板代码,提升了开发效率。 但过度使用全局模块会带来耦合和维护成本,建议谨慎使用,保持模块边界清晰。

动态模块

动态模块 = 运行时按需生成的模块,支持基于传入参数(如配置或实体)动态构造 providers、exports,适合封装「数据库连接」「缓存模块」「多租户模块」这类高度可配置的通用能力。

Nest 中的动态模块允许你创建可以在运行时配置的模块

  • 静态模块:代码写死,@Module({ ... }) 中的 providers 是固定的。
  • 动态模块:代码运行时动态生成,按需返回不同的模块结构

示例:数据库模块的 forRoot()

ts
@Module({
  providers: [Connection],
  exports: [Connection],
})
export class DatabaseModule {
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}
  • forRoot() 是一个静态工厂方法,返回一个 DynamicModule 对象;
  • 这个对象中可以包含一切动态元信息:module、providers、exports;
  • 可以根据传入的 entitiesoptions 构造不同的 providers;
  • 用法像这样:
ts
@Module({
  imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}

这样就能针对 AppModule 的需求动态定制数据库服务内容。

动态模块还能全局注册(慎用)

ts
return {
  global: true,
  module: DatabaseModule,
  providers: [...],
  exports: [...],
};

全局模块 + 动态模块 = 所有模块都能用它,无需 imports; 但 Nest 官方明确提示:慎用 global 模块,防止模块耦合失控。

动态模块可以被其他模块重新导出

ts
@Module({
  imports: [DatabaseModule.forRoot([User])],
  exports: [DatabaseModule], // 注意:这里 exports 是模块名,不是 forRoot()
})
export class AppModule {}

Nest 会自动透传动态模块返回的 providers 和 exports,你不需要手动导出 forRoot()

概念说明
forRoot()通常用于返回配置好的动态模块(同步)
forRootAsync()异步配置版本,适用于从 configService 获取参数
DynamicModuleNest 的“可编程模块结构”接口
适用场景数据库、Redis、MQ、ConfigService、多租户等“需传参初始化”的模块

动态模块让你可以把复杂、可配置的能力(如数据库)封装成“灵活工厂”,使用时传参自动生成配置好的模块,既复用代码,又保持模块化。

最后更新时间: