0Pricing
NestJS Enterprise Backend APIs · レッスン

設定可能な動的モジュールの構築

ConfigurableModuleBuilderを使ってforRootおよびforRootAsyncプロバイダーを作成し、再利用可能なテナントモジュールを実装します。

「設定可能な動的モジュールの構築」はCoddyKit上の無料NestJS Enterprise Backend APIsレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応の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.

よくある質問

「設定可能な動的モジュールの構築」レッスンは無料ですか?

はい。「設定可能な動的モジュールの構築」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、NestJS Enterprise Backend APIsコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 NestJS Enterprise Backend APIsコースには全4レッスンが含まれています。

「設定可能な動的モジュールの構築」で何を学びますか?

ConfigurableModuleBuilderを使ってforRootおよびforRootAsyncプロバイダーを作成し、再利用可能なテナントモジュールを実装します。 ブラウザで直接実行するハンズオンコードでNestJS Enterprise Backend APIsを演習し、24時間対応の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. MiddlewareとAsyncLocalStorageによるテナント解決
  2. テナントごとのスキーマによるデータベース接続
  3. 設定可能な動的モジュールの構築
  4. リクエストスコープのプロバイダーとトレードオフ
← NestJS Enterprise Backend APIsに戻る