asynchronousnestjsconfig

How to create a Nestjs module based on async config?


I'm trying to create a DB module like so:

const dbProvider = {
  provide: 'DB',
  useFactory: async (configService:ConfigService) => {
    const dbUrl = configService.get<string>('DB_URL')
    return Knex({
      client: 'pg',
      connection: dbUrl
    })
  },
  inject: [ConfigService]
}

@Module({
  providers: [ConfigService, dbProvider],
  exports: [dbProvider],
})
export class DbModule {}

This is the AppModule definition:

@Module({
  controllers: [AppController],
  providers: [Logger, AppService, {
    provide: ConfigService,
    useFactory: getConfigFactory(['DB_URL']),
  }],
  exports: [ConfigService]
})
export class AppModule {}

and:

export function getConfigFactory(paramsToLoad: string[]) {
    return async () => {await getConfigService(paramsToLoad)}
}
export async function getConfigService(paramsToLoad: string[]) {

    const paramStoreParams = await loadParamStore(paramsToLoad)
    return new ConfigService(paramStoreParams)
}

loadParamStore uses SSM to fetch parameters from SSM

The issue is, that when the DB setup is performed (above), the ConfigService only contains the envs taken from .env, DB_URL is only loaded at a later stage (verified), so at the time of building knex, DB_URL is not yet available.

Is there a correct Nestjs way to achieve such functionality?


Solution

  • First off, start by removing your custom getConfigFactory() and getConfigService() functions. You should not create the ConfigService instance yourself. It is instantiated for you by importing the ConfigModule. Sure, you can new it up yourself and pass it data, but that's meant for internal usage by the ConfigModule.

    If you want to load configuration from an external source, SSM in your case, then use the custom configuration file feature.

    https://docs.nestjs.com/techniques/configuration#custom-configuration-files

    Add a new file, e.g. external-config.ts to your project. Here you can code how to load your external configuration. It should return a factory function that returns a configuration object.

    export interface SsmConfiguration {
      database: {
        url: string;
      };
    }
    
    export async function loadExternalConfiguration(): Promise<SsmConfiguration> {
      // Load the configuration from SSM here.
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve({
            database: {
              url: 'localhost',
            },
          });
        }, 1000);
      });
    }
    

    By the way, this can still be combined with .env files.

    Next import the ConfigModule as a global module in the AppModule. This way its providers (ConfigService) can be used in other modules without having to re-import the ConfigModule. Use its forRoot() method and specify the custom factory function created earlier in the load option.

    @Module({
      imports: [
        ConfigModule.forRoot({
          isGlobal: true,
          // You can specify multiple config factory functions.
          load: [loadExternalConfiguration],
        }),
        DbModule
        ...
      ]
      controllers: [AppController],
      providers: [...]
    })
    export class AppModule {}
    

    Last, but not least inject the ConfigService in the factory function that returns the Knex instance. At that time you should be able to read the configuration the ConfigService has read for you. Just make sure to use the correct dotted notation to access the properties of the configuration object returned by your custom factory function.

    @Module({
      imports: [],
      providers: [
        {
          provide: DB_TOKEN,
          useFactory: (config: ConfigService) => {
            const url = config.get<string>('database.url');
            return Knex({...})
          },
          inject: [ConfigService],
        },
      ],
      exports: [DB_TOKEN],
    })
    export class DbModule {}
    

    If you run the application you'll notice a 1s delay during the startup when its simulating a delay when "fetching" the configuration.

    You can now inject the provider identified by the DB token in your providers that are part of the AppModule.