Building Configurable Dynamic Modules
Author forRoot and forRootAsync providers with ConfigurableModuleBuilder for reusable tenant modules.
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 timesforFeature()— 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],
};
}
}All lessons in this course
- Tenant Resolution via Middleware and AsyncLocalStorage
- Schema-per-Tenant Database Connections
- Building Configurable Dynamic Modules
- Request-Scoped Providers and Their Trade-offs