Creating Dynamic Modules in Nest JS Part-2

tkssharma - Mar 31 '22 - - Dev Community

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,
      }),
    }),
Enter fullscreen mode Exit fullscreen mode

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 and forRoot
  • 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}`,
        }
      }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
export const HTTP_CLIENT_MODULE_OPTIONS = "HttpClientModuleOptions";
export const HTTP_CLIENT_TOKEN = "HttpClientToken";
export const HTTP_CLIENT_MODULE = "HttpClientModule";
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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],
    }
Enter fullscreen mode Exit fullscreen mode
@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,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
Enter fullscreen mode Exit fullscreen mode

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})
  ]
})
Enter fullscreen mode Exit fullscreen mode

useFactory

@Module({
  imports: [HttpClientModule.forRootAsync({
    useFactory: () => {
      return {
        host: "localhost",
        port: 5432,
        database: "nest",
        user: "john",
        password: "password"
      }
    }
  })]
})
Enter fullscreen mode Exit fullscreen mode

useExisting

@Module({
  imports: [HttpClientModule.registerAsync({
    useExisting: ConfigService
  })]
})
Enter fullscreen mode Exit fullscreen mode

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],
    };
  }
Enter fullscreen mode Exit fullscreen mode

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,
      },
    ];
  }
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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,
      }),
    })
Enter fullscreen mode Exit fullscreen mode

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,
    },
  ],
}
Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .