Dynamic Modules

Configure a Module at Import Time

Dynamic Modules

A dynamic module exposes a static `register()` or `forRoot()` that returns a configured module — how libraries like `@nestjs/config` work.

4 min read Level 3/5 #nestjs#modules#advanced
What you'll learn
  • Define a forRoot returning a DynamicModule
  • Pass options through to providers via a custom token
  • Use forRootAsync when options depend on other providers

A regular @Module() is static — its providers and imports are fixed at build time. A dynamic module is one that takes parameters when you import it. This is how JwtModule.register({...}), TypeOrmModule.forRoot(), and every other library you import with arguments works.

A forRoot Pattern

The module class stays mostly empty. The work happens in a static method that returns a DynamicModule object.

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

export interface ConfigOptions {
  envPath: string;
  isGlobal?: boolean;
}

@Module({})
export class ConfigModule {
  static forRoot(options: ConfigOptions): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        { provide: 'CONFIG_OPTIONS', useValue: options },
        ConfigService,
      ],
      exports: [ConfigService],
      global: options.isGlobal ?? false,
    };
  }
}

The consumer imports it with arguments:

@Module({
  imports: [ConfigModule.forRoot({ envPath: '.env', isGlobal: true })],
})
export class AppModule {}

Reading the Options

Inside the module, providers read the options via the token you registered.

@Injectable()
export class ConfigService {
  private readonly values: Record<string, string>;

  constructor(@Inject('CONFIG_OPTIONS') opts: ConfigOptions) {
    this.values = dotenv.parse(fs.readFileSync(opts.envPath));
  }

  get(key: string) {
    return this.values[key];
  }
}

register vs forRoot vs forFeature

A convention has emerged:

  • register(options) — per-import, no side effects (e.g. JwtModule.register)
  • forRoot(options) — the global, app-wide configuration; imported once
  • forFeature(options) — register feature-specific resources (entities, queues) under an already-rooted module

Pick the name that matches how often consumers should call it.

forRootAsync — Options From Config

What if your options come from another provider, like ConfigService? Add an Async variant that takes a useFactory.

static forRootAsync(opts: {
  useFactory: (...args: any[]) => Promise<DbOptions> | DbOptions;
  inject?: any[];
}): DynamicModule {
  return {
    module: DatabaseModule,
    providers: [
      {
        provide: 'DB_OPTIONS',
        useFactory: opts.useFactory,
        inject: opts.inject ?? [],
      },
      DatabaseService,
    ],
    exports: [DatabaseService],
  };
}

// Usage
DatabaseModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (cfg: ConfigService) => ({ url: cfg.get('DB_URL') }),
});

Dynamic modules are the API contract third-party Nest libraries expose. Once you’ve written one, every “fancy” library starts to look familiar.

Global Modules →