title: 'Dynamic modules · Nael Framework' description: 'Design modules that accept configuration via register/registerAsync so features can be reused across applications.'

Fundamentals

Dynamic modules

Dynamic modules expose a factory API—usually called register() or forRoot()—so consumers can configure providers on the fly. They are the building block for reusable libraries such as Config, Auth, or Scheduler.

Synchronous register/forRoot

Return a DynamicModule object with the module, providers, and exports fields. The calling module decides the configuration, while your feature stays stateless.

MailerModule.register
import { DynamicModule, Module } from '@nl-framework/core';type MailerModuleOptions = {  transport: string;  defaults?: Record<string, unknown>;};@Module({})export class MailerModule {  static register(options: MailerModuleOptions): DynamicModule {    return {      module: MailerModule,      providers: [        MailerService,        { provide: MAILER_OPTIONS, useValue: options },      ],      exports: [MailerService],    };  }}

Consuming dynamic modules

Feature modules import the configured version just like any other module. Each call to register() produces its own provider graph, so different features can point to different transports without interfering with each other.

UsersModule importing MailerModule
// feature/users/users.module.tsimport { Module } from '@nl-framework/core';@Module({  imports: [    MailerModule.register({      transport: 'smtp://send.example.com',      defaults: { from: 'support@example.com' },    }),  ],  providers: [UsersService],})export class UsersModule {}

Asynchronous configuration

Use registerAsync()/forRootAsync() when you need to read environment variables, fetch secrets, or build the option object asynchronously. You can delegate to useFactory, useClass, or useExisting just like any other provider.

registerAsync with ConfigService
import { DynamicModule, Module } from '@nl-framework/core';import { ConfigModule, ConfigService } from '@nl-framework/config';type MailerModuleAsyncOptions = {  imports?: DynamicModule[];  inject?: any[];  useFactory: (...args: any[]) => Promise<MailerModuleOptions> | MailerModuleOptions;};@Module({})export class MailerModule {  static registerAsync(options: MailerModuleAsyncOptions): DynamicModule {    return {      module: MailerModule,      imports: options.imports ?? [],      providers: [        MailerService,        {          provide: MAILER_OPTIONS,          useFactory: options.useFactory,          inject: options.inject ?? [],        },      ],      exports: [MailerService],    };  }}// root module@Module({  imports: [    ConfigModule.forRoot({ isGlobal: true }),    MailerModule.registerAsync({      imports: [ConfigModule],      inject: [ConfigService],      useFactory: async (config: ConfigService) => ({        transport: config.get('smtp.url'),        defaults: { from: config.get('smtp.from') },      }),    }),  ],})export class AppModule {}

Sharing configured modules

If multiple feature modules need the same configuration (e.g., one SMTP transport), wrap the dynamic module in a shared module and export it. Downstream modules simply import the shared module to reuse the providers.

Re-export a configured module
@Module({  imports: [MailerModule.register({ transport: 'smtp://...' })],  exports: [MailerModule],})export class MessagingSharedModule {}@Module({  imports: [MessagingSharedModule],})export class NotificationsModule {}

This mirrors NestJS’s forRoot()/forFeature() pattern and keeps your dependency graph explicit.