Creating Dynamic Modules in Nest JS Part-2
Please checkout Part-1 of this blog series before moving to Part-2 to get basic idea about dynamic Modules. here is the link
https://dev.to/tkssharma/creating-dynamic-modules-in-nest-js-part-1-2n0d
Code :
https://github.com/tkssharma/blogs/tree/master/nestjs-dynamic-module
I am starting just after finishing part of Part-1 of this blog.
Okay we have a use-case of creating External HTTP client as a nestjs dynamic Module, This Module will act as a http service using which we can make api calls same as axios or httpClient
This is just for Demo and based on this we can create other nestjs dynamic Modules which can be plugged anywhere in any project
Our final Goal to have something like this
We should be able to expose all different methods like forRoot and forRootAsync from dynamic Module
forRootAsync
should return Dynamic Module
HttpClientModule.forRootAsync({
imports: [AppConfigModule],
inject: [AppConfigService],
useFactory: (config: AppConfigService) => ({
apiUrl: config.platformApi.baseUrl,
apiKey: config.platformApi.apiKey,
}),
}),
Lets get started
- Using this Module we want to expose service methods which can deal with http calls to external world
- we need a plain ES6 service which we can use with Providers
- we need HttpClient Module to have all these methods as static
forRootAsync
andforRoot
- Injectable Providers and Tokens we need
we will write service which wil get HttpClientModule options and will use its methods
For a HttpClient module, options can be a url and api key or any custom header we want to pass in api calls
export class HttpClientService {
private readonly apiUrl: string = "";
private readonly apiKey: string = "";
constructor(
@Inject(HTTP_CLIENT_MODULE_OPTIONS)
private readonly options: HttpClientModuleOptions
) {
this.apiUrl = this.options.apiUrl;
this.apiKey = this.options.apiKey;
}
public async fetchData(method: string, payload?: any) {
return axios({
{
method,
url: `${this.apiUrl}/health`,
data,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
}
}
);
}
}
export const HTTP_CLIENT_MODULE_OPTIONS = "HttpClientModuleOptions";
export const HTTP_CLIENT_TOKEN = "HttpClientToken";
export const HTTP_CLIENT_MODULE = "HttpClientModule";
Create a provider which can take HttpClientModuleOptions
and return use a provider, Provider is using Injectable Token HTTP_CLIENT_TOKEN and value for that Injectable token is instance of HttpClientService service
export function createHttpClientProvider(
options: HttpClientModuleOptions
): Provider {
return {
provide: HTTP_CLIENT_TOKEN,
useValue: getHttpClientModuleOptions(options),
};
}
export const getHttpClientModuleOptions = (
options: HttpClientModuleOptions
): HttpClientService => new HttpClientService(options);
Now we can use this createHttpClientProvider
function in HttpClientModule for adding Providers
Here is the important Part we are creating static methods forRoot and forRootAsync both methods
should return module like structure
{
module: HttpClientModule,
imports: options.imports,
providers: [...this.createAsyncProviders(options), provider],
exports: [provider],
}
@Global()
@Module({})
export class HttpClientModule {
public static forRoot(options: HttpClientModuleOptions): DynamicModule {
const provider: Provider = createHttpClientProvider(options);
return {
module: HttpClientModule,
providers: [provider],
exports: [provider],
};
}
public static forRootAsync(
options: HttpClientModuleAsyncOptions
): DynamicModule {
const provider: Provider = {
inject: [HTTP_CLIENT_MODULE_OPTIONS],
provide: HTTP_CLIENT_TOKEN,
useFactory: async (options: HttpClientModuleOptions) =>
getHttpClientModuleOptions(options),
};
return {
module: HttpClientModule,
imports: options.imports,
providers: [...this.createAsyncProviders(options), provider],
exports: [provider],
};
}
private static createAsyncProviders(
options: HttpClientModuleAsyncOptions
): Provider[] {
if (options.useExisting || options.useFactory) {
return [this.createAsyncOptionsProvider(options)];
}
const useClass = options.useClass as Type<HttpClientModuleFactory>;
return [
this.createAsyncOptionsProvider(options),
{
provide: useClass,
useClass,
},
];
}
private static createAsyncOptionsProvider(
options: HttpClientModuleAsyncOptions
): Provider {
if (options.useFactory) {
return {
provide: HTTP_CLIENT_MODULE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
};
}
const inject = [
(options.useClass ||
options.useExisting) as Type<HttpClientModuleFactory>,
];
return {
provide: HTTP_CLIENT_MODULE_OPTIONS,
useFactory: async (optionsFactory: HttpClientModuleFactory) =>
await optionsFactory.createHttpModuleOptions(),
inject,
};
}
}
Lets de-code the forRoot Implementation Here we are returning DynamicModule and its using provider returned from createHttpClientProvider and exporting same, createHttpClientProvider is nothing but instance of httpClientService
public static forRoot(options: HttpClientModuleOptions): DynamicModule {
const provider: Provider = createHttpClientProvider(options);
return {
module: HttpClientModule,
providers: [provider],
exports: [provider],
};
}
// createHttpClientProvider will return this
{
provide: HTTP_CLIENT_TOKEN,
useValue: new HttpClientService(options)
}
Variant Forms of Asynchronous Options Providers
Asynchronous providers
At times, the application start should be delayed until one or more asynchronous tasks are completed. For example, you may not want to start accepting requests until the connection with the database has been established. You can achieve this using asynchronous providers.
https://docs.nestjs.com/fundamentals/custom-providers
useClass
@Module({
imports: [
HttpClientModule.forRootAsync({ useClass: ConfigService})
]
})
useFactory
@Module({
imports: [HttpClientModule.forRootAsync({
useFactory: () => {
return {
host: "localhost",
port: 5432,
database: "nest",
user: "john",
password: "password"
}
}
})]
})
useExisting
@Module({
imports: [HttpClientModule.registerAsync({
useExisting: ConfigService
})]
})
Supporting Multiple Async Options Providers Techniques
We're in the home stretch. We're going to focus now on generalizing and optimizing our forRootAsync() method to support the additional techniques described above. When we're done, our module will support all three techniques:
- useClass - to get a private instance of the options provider.
- useFactory - to use a function as the options provider.
- useExisting - to re-use an existing (shared, SINGLETON) service as the options provider. Lets check the code for all these cases
public static forRootAsync(
options: HttpClientModuleAsyncOptions
): DynamicModule {
const provider: Provider = {
inject: [HTTP_CLIENT_MODULE_OPTIONS],
provide: HTTP_CLIENT_TOKEN,
useFactory: async (options: HttpClientModuleOptions) =>
getHttpClientModuleOptions(options),
};
return {
module: HttpClientModule,
imports: options.imports,
providers: [...this.createAsyncProviders(options), provider],
exports: [provider],
};
}
Now as we know options object can be of these different type so we have to handle that
private static createAsyncProviders(
options: HttpClientModuleAsyncOptions
): Provider[] {
if (options.useExisting || options.useFactory) {
return [this.createAsyncOptionsProvider(options)];
}
const useClass = options.useClass as Type<HttpClientModuleFactory>;
return [
this.createAsyncOptionsProvider(options),
{
provide: useClass,
useClass,
},
];
}
Lets also have a look on HttpClientModuleAsyncOptions with all there name options
export interface HttpClientModuleOptions {
apiUrl: string;
apiKey: string;
}
export interface HttpClientModuleFactory {
createHttpModuleOptions: () =>
| Promise<HttpClientModuleOptions>
| HttpClientModuleOptions;
}
export interface HttpClientModuleAsyncOptions
extends Pick<ModuleMetadata, "imports"> {
inject?: any[];
useClass?: Type<HttpClientModuleFactory>;
useExisting?: Type<HttpClientModuleFactory>;
useFactory?: (
...args: any[]
) => Promise<HttpClientModuleOptions> | HttpClientModuleOptions;
}
After we have all these ready we can use this module in all different ways like
HttpClientModule.forRootAsync({
imports: [AppConfigModule],
inject: [AppConfigService],
useFactory: (config: AppConfigService) => ({
apiUrl: config.platformApi.baseUrl,
apiKey: config.platformApi.apiKey,
}),
})
Another option
@Module({
imports: [HttpClientModule.forRootAsync({
useExisting: AppConfigService
})]
})
We could expect a dynamic module to be constructed with the following properties:
{
module: HttpClientModule,
imports: [],
providers: [
{
provide: HTTP_CLIENT_MODULE_OPTIONS,
useFactory: async (optionsFactory: HttpClientModuleFactory) =>
await optionsFactory.createHttpModuleOptions(),
inject,
},
],
}
Conclusion
The patterns is used in all popular modules like @nestjs/jwt, @nestjs/passport and @nestjs/typeorm. Hopefully you now see not only how powerful these patterns are, but how you can make use of them in your own project.
References
- https://dev.to/nestjs/advanced-nestjs-how-to-build-completely-dynamic-nestjs-modules-1370
- https://docs.nestjs.com/fundamentals/dynamic-modules
- https://docs.nestjs.com/
- Blog Originally published at tkssharma.com
Top comments (2)
Hi, I'm stuck with this error trying to get it to work, I import it into the AppModule like this:
import { HttpClientModule } from './http-client/http-client.module';
@Module({
...
imports: [
HttpClientModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
apiUrl: config.get('API_URL'),
apiKey: config.get('API_KEY'),
}),
}),
],
...
Then I get this message:
Nest can't resolve dependencies of the HttpClientService (?). Please make sure that the argument HttpClientModuleOptions at index [0] is available in the AppModule context.
How do I make it work?
Thanks.
Same problem here...