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.
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.
// 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.
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.
@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.