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.
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 onceforFeature(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 →