The wizardry needed to create sibling dependencies DynamicModules in NestJs
Very likely my own misunderstanding, or maybe a shortcoming of the DI in NestJs or the documentation, I found that creating a configurable module that imported sibling can consume did my utter nut in. The broken branch demonstrates how I understood the DI to work (but it did not) while the master
is how I managed to do it.
While this is a simplfied project to illustrate, I can't help but think this is an area that could be improved on (or at least made clearer in the documentation). Common real-world useage would be Database modules, configuration modules, etc... often worked around by making the module @Global
(which feels... dirty?)
Creating a configurable module is fairly straight forward:
- add static methods to module
- configure and return a DynamicModule
@Module({})
export class CharactersModule {
static forRootAsync(config: ICharactersAsyncConfig): DynamicModule {
const provider = {
provide: CHARACTERS,
useFactory: config.useFactory,
inject: config.inject || [],
};
return {
module: CharactersModule,
providers: [provider, CharactersService],
exports: [CharactersService],
};
}
}
The problem I ran into is when another DynamicModule depends on the configured version
@Module({})
export class ChaptersModule {
static registerAsync(config: IChaptersAsyncConfig): DynamicModule {
const provider = {
provide: CHAPTERS,
useFactory: config.useFactory,
inject: config.inject || [],
};
return {
module: ChaptersModule,
imports: [CharactersModule],
providers: [provider, ChaptersService],
exports: [ChaptersService],
};
}
}
Which I would have HOPED would be wired up here:
@Module({
imports: [
CharactersModule.forRootAsync({
useFactory: () => characterData,
}),
ChaptersModule.registerAsync({
useFactory: () => chapterData,
}),
],
controllers: [AppController],
providers: [AppService, CharactersService],
})
export class AppModule {}
However NestJS doesn't appear to share module exports with siblings so you end up with this message:
Error: Nest can't resolve dependencies of the ChaptersService (CHAPTERS, ?). Please make sure that the argument CharactersService at index [1] is available in the ChaptersModule context.
This is the only way I have found to make this work. No idea if its the correct way
- Add an optional
requires?: any[]
to the async interface - Destruct into the returning DynamicModule
@Module({})
export class ChaptersModule {
static registerAsync(config: IChaptersAsyncConfig) : DynamicModule{
const provider = {
provide: CHAPTERS,
useFactory: config.useFactory,
inject: config.inject || [],
}
return {
module: ChaptersModule,
imports: [...config.requires || []], // import pre-configured
providers: [provider, ChaptersService],
exports: [ChaptersService]
}
}
}
Then in the consuming module, capture the dependency into a seperate variable
// Pre-Configure any dependent modules
export const configured = [
CharactersModule.forRoot(characterData)
]
@Module({
imports: [
...configured, // include configured modules into scope
ChaptersModule.registerAsync({
requires: configured, // pass configured modules
useFactory: () => chapterData
})
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}