0Pricing
NestJS Enterprise Backend APIs · 课时

构建可配置的动态模块

使用 ConfigurableModuleBuilder 编写 forRoot 和 forRootAsync 提供者,构建可复用的租户模块。

构建可配置的动态模块 是 CoddyKit 上的免费 NestJS Enterprise Backend APIs 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 NestJS Enterprise Backend APIs 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 NestJS Enterprise Backend APIs 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Why Dynamic Modules?

A regular NestJS module exports a fixed set of providers. But a reusable module — like a tenant resolver, a cache client, or an HTTP wrapper — needs configuration from the consumer: a connection string, an API key, a tenant strategy.

A dynamic module is a module whose providers, imports, and exports are computed at import time from caller-supplied options. The convention is a static factory method, almost always named forRoot() (or register()), that returns a DynamicModule object.

  • forRoot() — configure once, globally (DB, tenant registry)
  • register() — configure per-import, possibly multiple times
  • forFeature() — register feature-scoped sub-resources

The DynamicModule Shape

A static factory must return an object matching the DynamicModule interface. The key extra field is module, which points back to the host class. Everything else mirrors a normal @Module() decorator.

Here a TenantModule.forRoot() turns caller options into a provider keyed by a token, then exports it so other modules can inject it.

import { DynamicModule, Module } from '@nestjs/common';

export interface TenantOptions {
  registryUrl: string;
  defaultTenant: string;
}

export const TENANT_OPTIONS = 'TENANT_OPTIONS';

@Module({})
export class TenantModule {
  static forRoot(options: TenantOptions): DynamicModule {
    return {
      module: TenantModule,
      providers: [{ provide: TENANT_OPTIONS, useValue: options }],
      exports: [TENANT_OPTIONS],
    };
  }
}

Consuming forRoot

The consumer imports the dynamic module by calling the factory, not just referencing the class. The returned options provider becomes injectable anywhere inside the module's scope.

A service injects the TENANT_OPTIONS token to read its configuration.

import { Inject, Injectable, Module } from '@nestjs/common';
import { TenantModule, TENANT_OPTIONS, TenantOptions } from './tenant.module';

@Injectable()
export class TenantService {
  constructor(@Inject(TENANT_OPTIONS) private readonly opts: TenantOptions) {}

  resolve(header?: string): string {
    return header ?? this.opts.defaultTenant;
  }
}

@Module({
  imports: [
    TenantModule.forRoot({
      registryUrl: 'https://registry.internal/tenants',
      defaultTenant: 'acme',
    }),
  ],
})
export class AppModule {}

The Problem: Async Configuration

Synchronous forRoot(options) works only when you already hold the literal values. In real backends the registryUrl lives in ConfigService, a secrets manager, or another async source.

That is why reusable modules expose a second factory: forRootAsync(). It lets the caller provide options via useFactory, useClass, or useExisting — and crucially, to inject other providers that resolve the values.

  • useFactory — inline async function returning the options
  • useClass / useExisting — a class implementing an options factory interface

Hand-Rolled forRootAsync

Before reaching for helpers, it is worth seeing the mechanics. forRootAsync accepts an async-options object, declares an imports list (so ConfigModule is visible), and wires the caller's useFactory into the options provider.

The factory's inject array feeds dependencies into useFactory, exactly like any provider.

import { DynamicModule, Module, Provider } from '@nestjs/common';
import { TenantOptions, TENANT_OPTIONS } from './tenant.module';

export interface TenantAsyncOptions {
  imports?: any[];
  inject?: any[];
  useFactory: (...args: any[]) => Promise<TenantOptions> | TenantOptions;
}

@Module({})
export class TenantModule {
  static forRootAsync(async: TenantAsyncOptions): DynamicModule {
    const optionsProvider: Provider = {
      provide: TENANT_OPTIONS,
      useFactory: async.useFactory,
      inject: async.inject ?? [],
    };
    return {
      module: TenantModule,
      imports: async.imports ?? [],
      providers: [optionsProvider],
      exports: [TENANT_OPTIONS],
    };
  }
}

Calling forRootAsync

Now the consumer pulls values from ConfigService at runtime. The imports field makes ConfigModule available inside the dynamic module so the factory can inject ConfigService.

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TenantModule } from './tenant.module';

@Module({
  imports: [
    ConfigModule.forRoot(),
    TenantModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        registryUrl: config.getOrThrow<string>('TENANT_REGISTRY_URL'),
        defaultTenant: config.get<string>('DEFAULT_TENANT', 'acme'),
      }),
    }),
  ],
})
export class AppModule {}

Enter ConfigurableModuleBuilder

Writing both factories by hand is boilerplate that every reusable module repeats. Since NestJS 9, ConfigurableModuleBuilder generates the forRoot/forRootAsync plumbing for you from a single options type.

You create a small *.module-definition.ts file that calls the builder and exports:

  • ConfigurableModuleClass — a base class your module extends
  • MODULE_OPTIONS_TOKEN — the injection token for the resolved options
  • OPTIONS_TYPE / ASYNC_OPTIONS_TYPE — typed signatures for the generated methods

Defining the Module Definition

The builder is generic over your options interface. Calling .build() returns everything the module needs. Your module then extends ConfigurableModuleClass and instantly gains forRoot() and forRootAsync().

import { ConfigurableModuleBuilder } from '@nestjs/common';

export interface TenantModuleOptions {
  registryUrl: string;
  defaultTenant: string;
}

export const {
  ConfigurableModuleClass,
  MODULE_OPTIONS_TOKEN,
  OPTIONS_TYPE,
  ASYNC_OPTIONS_TYPE,
} = new ConfigurableModuleBuilder<TenantModuleOptions>().build();

Wiring the Module Class

The host module extends the generated base class and registers its real providers. Services inject MODULE_OPTIONS_TOKEN to read the resolved options — whether they came from forRoot or forRootAsync, the token is the same.

Note: when you add your own providers/exports alongside the generated ones, keep the @Module() decorator — the builder merges its dynamic part with your static metadata.

import { Module } from '@nestjs/common';
import {
  ConfigurableModuleClass,
  MODULE_OPTIONS_TOKEN,
} from './tenant.module-definition';
import { TenantService } from './tenant.service';

@Module({
  providers: [TenantService],
  exports: [TenantService, MODULE_OPTIONS_TOKEN],
})
export class TenantModule extends ConfigurableModuleClass {}

Custom Method Name & Extra Options

The builder is configurable. Use .setClassMethodName('register') when per-import registration reads better than forRoot. Use .setExtras() to add fields that are not part of the injected options — typically isGlobal — and a transform that injects them into the DynamicModule (e.g. setting global: true).

import { ConfigurableModuleBuilder } from '@nestjs/common';
import { TenantModuleOptions } from './tenant.types';

export interface TenantExtras {
  isGlobal?: boolean;
}

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
  new ConfigurableModuleBuilder<TenantModuleOptions>()
    .setExtras<TenantExtras>(
      { isGlobal: false },
      (definition, extras) => ({
        ...definition,
        global: extras.isGlobal,
      }),
    )
    .setClassMethodName('register')
    .build();

Multi-Tenancy Payoff

Put together, a tenant module becomes a drop-in dependency for any service in the platform. A request-scoped provider can resolve the active tenant from a header, falling back to the configured default — all driven by options the consuming app supplied via register/registerAsync.

The key insight: configuration flows through one token (MODULE_OPTIONS_TOKEN), so internal services never care whether setup was sync or async, literal or factory-resolved.

import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
import { MODULE_OPTIONS_TOKEN } from './tenant.module-definition';
import { TenantModuleOptions } from './tenant.types';

@Injectable({ scope: Scope.REQUEST })
export class TenantContext {
  constructor(
    @Inject(MODULE_OPTIONS_TOKEN) private readonly opts: TenantModuleOptions,
    @Inject(REQUEST) private readonly req: Request,
  ) {}

  get tenantId(): string {
    const header = this.req.headers['x-tenant-id'];
    return (Array.isArray(header) ? header[0] : header) ?? this.opts.defaultTenant;
  }
}

Quick Check

Your reusable TenantModule must read its registryUrl from ConfigService, which itself loads asynchronously at bootstrap. Which approach lets the consumer wire this correctly?

Recap

You built a configurable dynamic module end to end:

  • A dynamic module returns a DynamicModule from a static factory (forRoot/register), turning caller options into a token-backed provider.
  • forRootAsync adds imports, inject, and useFactory so options can be resolved from ConfigService or other async sources.
  • ConfigurableModuleBuilder generates both factories from one options type, exposing ConfigurableModuleClass and MODULE_OPTIONS_TOKEN.
  • .setClassMethodName() renames the factory; .setExtras() adds out-of-band flags like isGlobal.
  • All internal services inject the same options token, staying agnostic to how configuration was supplied — the foundation for reusable multi-tenant modules.

常见问题解答

「构建可配置的动态模块」课时是免费的吗?

是的 — 「构建可配置的动态模块」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 NestJS Enterprise Backend APIs 课程的其余内容,请升级到 CoddyKit PRO。 NestJS Enterprise Backend APIs 课程共包含 4 节课。

「构建可配置的动态模块」这节课中我会学到什么?

使用 ConfigurableModuleBuilder 编写 forRoot 和 forRootAsync 提供者,构建可复用的租户模块。 你通过在浏览器中直接运行的动手代码来练习 NestJS Enterprise Backend APIs,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 NestJS Enterprise Backend APIs 需要有经验吗?

无需任何先前经验。CoddyKit 上的 NestJS Enterprise Backend APIs 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「构建可配置的动态模块」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 NestJS Enterprise Backend APIs 课中编写并运行代码吗?

能。每节 NestJS Enterprise Backend APIs 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 通过中间件和 AsyncLocalStorage 解析租户
  2. 每个租户独立的数据库模式连接
  3. 构建可配置的动态模块
  4. 请求作用域提供者及其权衡
← 返回 NestJS Enterprise Backend APIs