Creating Dynamic Module Nest JS Part-2

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://tkssharma.com/nestjs-creating-dynamic-modules/

Code : https://github.com/tkssharma/blogs/tree/master/nestjs-dynamic-module

Okay i 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 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}`,
        }
      }
    );
  }
}
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

useClass

What we've built so far allows us to configure the MassiveModule by handing it a class whose purpose is to dynamically provide connection options. Let's just remind ourselves again how that looks from the consumer perspective:

@Module({
  imports: [
    HttpClientModule.forRootAsync({ useClass: ConfigService})
  ]
})

We can refer to this as configuring our dynamic module with a useClass technique (AKA a class provider). Are there other techniques? You may recall seeing several other similar patterns in the Custom providers chapter. We can model our forRootAsync() interface based on those patterns. Let's sketch out what those techniques would look like from a consumer module perspective, and then we can easily add support for them.

useFactory

Factory Providers: useFactory While we did make use of a factory in the previous section, that was strictly internal to the dynamic module construction mechanics, not a part of the callable API. What would useFactory look like when exposed as an option for our forRootAsync() method?

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

In the sample above, we supplied a very simple factory in place, but we could of course plug in (or pass in a function implementing) any arbitrarily sophisticated factory as long as it returns an appropriate connections object.

useExisting

This sometimes-overlooked construct is actually extremely useful. In our context, it means we can ensure that we re-use an existing options provider rather than instantiating a new one. For example, useClass: ConfigService will cause Nest to create and inject a new private instance of our ConfigService. In the real world, we'll usually want a single shared instance of the ConfigService injected anywhere it's needed, not a private copy. The useExisting technique is our friend here. Here's how it would look:

@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.

I'm going to jump right to the code, as we're all getting weary now 😉. I'll describe the key elements below.

  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 illustrated above are used throughout Nest's add-on 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

Comments