-
Notifications
You must be signed in to change notification settings - Fork 0
Labels
help wantedExtra attention is neededExtra attention is neededquestionFurther information is requestedFurther information is requested
Description
Discussed in #34
Originally posted by Evanion June 28, 2025
Overview
This RFC proposes a refactor of the DynamicModule system in NexusDI to provide a type-safe, ergonomic, and unified API for module configuration. The goal is to eliminate boilerplate for module authors, support both static and DI-injected factory config, and ensure type safety for application developers.
Motivation
- Reduce boilerplate: Module authors should only declare the config token and expected config type, not write custom static methods.
- Type safety: The config passed to
.config()or.configAsync()should be type-checked against the module’s expected config type. - Support DI-injected factories: Allow config to be provided via a factory function with dependencies (like
useFactory+depsin providers). - Async support: Support async config factories and enforce
awaitbefore registration. - Consistency: All modules use the same config pattern, making documentation and onboarding easier.
- Optional global flag: Allow modules/providers to be marked as global for cross-cutting concerns.
Proposed Features
- Generic, type-safe config API:
DynamicModule<TConfig>enforces the config type for.config()and.configAsync(). - Unified config method:
The base class provides a staticconfig()method that supports:- Static config objects
- Factory config with DI (
useFactory+deps) - Async factories
- No custom static methods:
Module authors only declare the config token and expected config type. - Global module/provider support:
Optionally, allow modules/providers to be marked as global. - Container integration:
The container accepts both plain module classes and module config objects, and enforcesawaitfor async config.
Example Usage
Module Author
export interface ITypeOrmConfig {
type: string;
host: string;
port: number;
// ...other options
}
export const DB_CONFIG = Symbol('DB_CONFIG');
@Module({
providers: [TypeOrmService],
})
export class TypeOrmModule extends DynamicModule<ITypeOrmConfig> {
static configToken = DB_CONFIG;
// No need to write a static config() method!
}Application Developer
import { ConfigService } from '@nexusdi/config';
@Module({
imports: [
// Static config (type-checked)
TypeOrmModule.config({
type: 'postgres',
host: 'localhost',
port: 5432,
}),
// Factory config with DI (type-checked)
TypeOrmModule.config({
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get('db.host'),
port: config.get('db.port'),
}),
deps: [ConfigService]
}),
// Async factory config with DI (type-checked)
await TypeOrmModule.configAsync({
useFactory: async (config: ConfigService) => {
const host = await config.getAsync('db.host');
return {
type: 'postgres',
host,
port: 5432,
};
},
deps: [ConfigService]
}),
]
})
export class AppModule {}Under the Hood
- The base
DynamicModuleprovides a static, genericconfig()andconfigAsync()method. - These methods accept:
- A static config object (
TConfig) - An object with
useFactoryanddeps(dependencies to inject)
- A static config object (
- The config value is registered under the module’s
configToken. - The container enforces that async configs are awaited before registration.
API Sketch
export abstract class DynamicModule<TConfig = any> {
static configToken: TokenType<TConfig>;
static config(
config:
| TConfig
| ProviderConfigObject<TConfig>
): ModuleConfig {
// Implementation: register provider for configToken
// with useValue or useFactory/inject
}
static async configAsync(
configProvider: ProviderConfigObject<Promise<TConfig>>
): Promise<ModuleConfig> {
// Implementation: resolve deps, call useFactory, return ModuleConfig
}
}Complexity & Implementation Review
What’s Already Supported (Low Complexity to Keep/Refine)
- Type-safe config via generics: Already present; just needs to be enforced and documented.
- Static config objects: Already supported via
.config(config: TConfig). - Async config factories: Already supported via
.configAsync(() => Promise<TConfig>), though not with DI.
What Would Add Some Complexity (But Is Reasonable)
- Factory Config with DI (
useFactory+deps):- Requires the container to resolve dependencies before calling the factory.
- Pattern is well-established (used in NestJS, Angular, etc.).
- Adds minimal bloat if you already have a DI system.
- Global Module/Provider Support:
- Only complex if you want true encapsulation; a simple flag is low bloat.
- Validation Hooks:
- Low complexity if optional and opt-in.
What Would Add Significant Bloat/Complexity (Not Recommended)
- Registering Promises Directly in the Container:
- Would complicate the container’s internal state and error handling.
- Recommendation: Do not support; enforce
awaitbeforeset().
- Live/Observable Config Injection:
- Would require proxies, subscriptions, and possibly re-injection or re-binding.
- Not needed for most use cases; can be opt-in via a separate utility.
Summary Table
| Feature/Option | Complexity | Bloat | Recommendation |
|---|---|---|---|
| Type-safe generics | Low | None | Keep |
| Static config | Low | None | Keep |
| Async config (awaited) | Low | None | Keep |
Factory + DI (useFactory) |
Moderate | Low | Add (well-established pattern) |
| Global module/provider | Moderate | Low | Add if needed, keep simple |
| Validation hooks/schemas | Low | Low | Add as opt-in |
| Registering promises in set() | High | High | Do not add |
| Live/observable config | High | High | Optional, not default |
Migration Notes
- Module authors:
- Remove custom static
config()/configAsync()methods. - Declare the config token and expected config type.
- Remove custom static
- App developers:
- Use the new unified
config()/configAsync()API. - Always
awaitthe result ofconfigAsync()before passing tocontainer.set().
- Use the new unified
Open Questions
- Should we support both sync and async factories in
.config()or require.configAsync()for async? - Should we allow modules/providers to be marked as global, and how should that be enforced?
- Should the container support registering promises directly, or always require
await? - How should validation be handled (e.g., via schema, manual, or both)?
Call for Feedback
- Does this pattern reduce boilerplate and improve DX for you?
- Are there use cases for module config that this pattern does not cover?
- Should we support additional config patterns (e.g., environment merging, validation hooks)?
- Any pain points with the current or proposed API?
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
help wantedExtra attention is neededExtra attention is neededquestionFurther information is requestedFurther information is requested
Type
Projects
Status
In progress