NestJs dependency injection: Implementing reusable code.

In the previous article, we discussed dependency injection containers and gave an overview of how it works in some modern Nodejs frameworks like Nestjs and Loopback.

In this article, we go a bit further to explain how this concept facilitates the creation of reusable code in Nestjs.

A module is a fundamental unit for wiring components. It provides mechanisms to import other modules it depends on, export functionalities needed by other modules, and make private within its scope any property that needs to be private.

In Nestjs, we can define static modules or dynamic modules.

Nestjs defines static a module using the @module decorator. With this decorator, you can define what the module imports (dependencies) and what the module exports. You can also define a module's core business logic as providers. If you need the services to be consumed by clients of the module, you export the services.

// Static module binding
  @Module({
    imports: [DatabaseProvider],
    providers: [UserService],
    exports: [UserService]
  })

From the code snippet above, any module that imports the User module as a dependency will be able to inject the User service as a dependency. This is possible because the User service has been exported from the User module. The idea is similar to what module.exports represents in the Node.js module system.

When a provider needs to be visible outside of a module, it is first exported from its host module, and then imported into its consuming module.

Static module binding does not allow a consuming module to alter its configuration. For example, a consuming module, cannot include other providers without having to alter the existing module code. This is not desirable for creating packages to be used in other applications. With dynamic modules, we can make a module as generic as possible to allow uses beyond any one application.

Dynamic modules give us the ability to pass parameters into the module being imported so we can change its behavior.

We shall use the Nestjs Mongoose library for illustration purposes.

The Mongoose module class (marked with the decorator @Module({})) has a couple of methods that export Dynamic modules.

The Dynamic module is an object factory with properties that define module imports, providers(defined by the useFactory method, and exports. It also has a property that marks the factory as a MongooseModule by assigning the MongooseModule class to the module property. This means whatever providers you pass into the module, can be used by other providers that need them in the module.

A dynamic module is nothing more than a module created at run-time, with the same exact properties as a static module, plus one additional property called module

// Module with asynchronous providers
  @Module({})
  export class MongooseModule {
    static forRootAsync(options: MongooseModuleAsyncOptions): DynamicModule {
        return {
          module: MongooseModule,
          imports: [MongooseCoreModule.forRootAsync(options)],
        };
      }
  }

The options argument of the forRootAsync method provides the information needed for database connection.

// Example Client module consuming MongoosModule
  import { ConfigService, ConfigModule } from '@nestjs/config';
  import { Module } from '@nestjs/common';
  import { MongooseModule } from '@nestjs/mongoose';

  export const mongodbConnection = (configService: ConfigService) => {
    const mapMongodbUrl = {
      development: configService.get<string>('DEV_DATABASE_URL'),
      production: configService.get<string>('PROD_DATABASE_URL'),
      test: configService.get<string>('TEST_DATABASE_URL'),
    };
    const env = configService.get<string>('NODE_ENV');
    return {
      uri: mapMongodbUrl[env],
      useNewUrlParser: true,
    };
  };

  const mongodbService = {
    imports: [ConfigModule],
    useFactory: mongodbConnection,
    inject: [ConfigService],
  };

  @Module({
    imports: [MongooseModule.forRootAsync(mongodbService)],
  })
  export class DatabaseModule {}

From the code, MongooseModule does not assume the type of database connection. We, however, need to pass the data needed to make a global database connection.

The information needed includes:

  • The factory that returns the connection details.
  • Any other dependency the factory needs to provide this information.

The factory requires the ConfigService to extract environment variables. By specifying the ConfigModule as an import, and indicating that the ConfigService is to be injected, the useFactory method will be able to make use of this service.

Note that because of how flexible the dynamic module is, we can decide to use any other library to get environment variables. We just need to ensure that they are registered in Nestjs and can be injected where needed.