Dynamic Modules & Provider Scopes
Dynamic Modules & Provider Scopes
So far modules have been static — fixed configurations. But how does ConfigModule.forRoot() accept options? That is a dynamic module: a module configured at import time. Paired with provider scopes, these are the tools that make NestJS modules reusable and flexible.
The forRoot / forFeature pattern
You have seen this everywhere: ConfigModule.forRoot(), TypeOrmModule.forRoot(), JwtModule.register(). Each is a static method returning a configured module on the fly.
Now consumers pass options when importing:
forRoot() configures a module once for the whole app (the root); forFeature() configures a slice for a specific feature module. register() is used when there is no global/feature distinction.
Provider scopes
By default every provider is a singleton — one instance shared app-wide. NestJS offers three scopes:
- DEFAULT (singleton) — one shared instance. Fast, the right choice for almost everything.
- REQUEST — a new instance per incoming request.
- TRANSIENT — a fresh instance for every consumer that injects it.
When you need REQUEST scope
Request scope is useful when a provider must hold data unique to the current request — the authenticated user, a per-request trace ID, or tenant info in a multi-tenant app. A request-scoped provider can even inject the request object itself:
The cost of request scope
AsyncLocalStorage context, instead of making a whole service request-scoped. Keep singletons singletons whenever you can.
Summary
Dynamic modules (the forRoot/forFeature/register pattern) return a DynamicModule configured at import time, which is how configurable library modules work. Provider scopes control instance lifetime: DEFAULT singletons are the norm; REQUEST and TRANSIENT exist for per-request or per-consumer state, but they bubble up and cost performance. This completes Phase 2 — you now understand how NestJS wires and configures everything.