Nest JS | Dependency Injection and Custom Providers

Nest JS | Dependency Injection and Custom Providers

Nest JS | Dependency Injection and Custom Providers

Title: Nest JS | Dependency Injection and Custom Providers with Code Examples

Description: In this blog post, we explore the powerful concept of dependency injection in Nest JS and delve into the world of custom providers. We will provide practical code examples throughout the post to illustrate the implementation of dependency injection and custom providers in Nest JS.

The blog post will cover the following topics:

  1. Introduction to Dependency Injection in Nest JS:

    • Explanation of dependency injection and its benefits.
    • Overview of how dependency injection works in Nest JS.
  2. Using Built-in Providers and Decorators:

    • Demonstrating the usage of built-in providers and decorators in Nest JS, such as @Injectable, @Inject, and @Optional.
    • Code examples showcasing how to define and inject dependencies using these decorators.
  3. Creating Custom Providers:

    • Detailed explanation of creating custom providers in Nest JS.
    • Code examples illustrating different types of custom providers, including class providers, value providers, and factory providers.
    • Step-by-step instructions on how to define and utilize custom providers in your Nest JS application.
  4. Scopes and Lifecycle Hooks:

    • Exploring the concept of scopes in Nest JS and how they affect the lifecycle of dependencies.
    • Code examples demonstrating the use of lifecycle hooks like onModuleInit and onModuleDestroy for managing dependency lifecycles.

Dependency injection is an inversion of control (IoC) technique wherein you delegate instantiation of dependencies to the IoC container (in our case, the NestJS runtime system), instead of doing it in your own code imperatively. Let's examine what's happening in this example from the Providers chapter.

First, we define a provider. The @Injectable() decorator marks the CatsService class as a provider.

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  findAll(): Cat[] {
    return this.cats;
  }
}

Then we request that Nest inject the provider into our controller class:


import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

Finally, we register the provider with the Nest IoC container:


import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}

What exactly is happening under the covers to make this work? There are three key steps in the process: In cats.service.ts, the @Injectable() decorator declares the CatsService class as a class that can be managed by the Nest IoC container. In cats.controller.ts, CatsController declares a dependency on the CatsService token with constructor injection:

constructor(private catsService: CatsService)

In app.module.ts, we associate the token CatsService with the class CatsService from the cats.service.ts file. We'll see below exactly how this association (also called registration) occurs.

When the Nest IoC container instantiates a CatsController, it first looks for any dependencies*. When it finds the CatsService dependency, it performs a lookup on the CatsService token, which returns the CatsService class, per the registration step (#3 above). Assuming SINGLETON scope (the default behavior), Nest will then either create an instance of CatsService, cache it, and return it, or if one is already cached, return the existing instance.

Injectable decorator for dependency injection

  1. Using the @Injectable decorator to define a service:
import { Injectable } from '@nestjs/common';

@Injectable()
export class MyService {
  constructor() {
    // Service initialization
  }

  // Service methods
}
  1. Injecting a service into a controller using constructor injection:
import { Controller, Get } from '@nestjs/common';
import { MyService } from './my.service';

@Controller('example')
export class MyController {
  constructor(private readonly myService: MyService) {}

  @Get()
  getExample(): string {
    return this.myService.getData();
  }
}

Creating a custom provider as a class provider:

import { Injectable } from '@nestjs/common';
import { MyDependency } from './my-dependency';

@Injectable()
export class MyProvider {
  constructor(private readonly myDependency: MyDependency) {}

  // Provider methods
}

Registering the custom provider in a module:

import { Module } from '@nestjs/common';
import { MyProvider } from './my-provider';

@Module({
  providers: [MyProvider],
})
export class AppModule {}

Creating a custom factory provider:

import { Injectable } from '@nestjs/common';

@Injectable()
export class MyFactoryProvider {
  createInstance(): MyDependency {
    // Factory logic to create and return an instance of MyDependency
  }
}

Registering the factory provider in a module:

import { Module } from '@nestjs/common';
import { MyFactoryProvider } from './my-factory-provider';

@Module({
  providers: [
    {
      provide: MyDependency,
      useFactory: (myFactoryProvider: MyFactoryProvider) =>
        myFactoryProvider.createInstance(),
      inject: [MyFactoryProvider],
    },
  ],
})
export class AppModule {}

Using the @Inject decorator to inject dependencies:

import { Injectable, Inject } from '@nestjs/common';
import { MyDependency } from './my-dependency';

@Injectable()
export class MyService {
  constructor(@Inject(MyDependency) private readonly myDependency: MyDependency) {}

  // Service methods
}

Creating a value provider custom provider

import { Injectable } from '@nestjs/common';

@Injectable()
export class MyValueProvider {
  provideValue(): string {
    // Return a value or configuration
    return 'Some value';
  }
}

Registering the value provider in a module:

import { Module } from '@nestjs/common';
import { MyValueProvider } from './my-value-provider';

@Module({
  providers: [
    {
      provide: 'MyValue',
      useValue: (myValueProvider: MyValueProvider) => myValueProvider.provideValue(),
      inject: [MyValueProvider],
    },
  ],
})
export class AppModule {}

Using the @Optional decorator to mark an optional dependency:

import { Injectable, Optional } from '@nestjs/common';
import { MyOptionalDependency } from './my-optional-dependency';

@Injectable()
export class MyService {
  constructor(@Optional() private readonly myDependency: MyOptionalDependency) {}

  // Service methods
}

Custom Providers (useValue, useClass, useExisting, useFactory) [Nest JS Docs]

Standard Provider

Let's take a closer look at the @Module() decorator. In app.module, we declare:

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})

The providers property takes an array of providers. So far, we've supplied those providers via a list of class names. In fact, the syntax providers: [CatsService] is short-hand for the more complete syntax: providers is app module is more powerful then we think it is, its an array of providers

providers: [
  {
    provide: CatsService,
    useClass: CatsService,
  },
];

Now that we see this explicit construction, we can understand the registration process. Here, we are clearly associating the token CatsService with the class CatsService. The short-hand notation is merely a convenience to simplify the most common use-case, where the token is used to request an instance of a class by the same name.

Custom providers

What happens when your requirements go beyond those offered by Standard providers? Here are a few examples:

These are 3 major options to create custom providers, which looks little same

  • You want to create a custom instance instead of having Nest instantiate (or return a cached instance of) a class
  • You want to re-use an existing class in a second dependency
  • You want to override a class with a mock version for testing

Value Providers useValue [Custom Provider]

import { CatsService } from './cats.service';

const mockCatsService = {
  /* mock implementation
  ...
  */
};

@Module({
  imports: [CatsModule],
  providers: [
    {
      provide: CatsService,
      useValue: mockCatsService,
    },
  ],
})
export class AppModule {}

Value can belongs to anything not only to a class

import { connection } from './connection';

@Module({
  providers: [
    {
      provide: 'CONNECTION',
      useValue: connection,
    },
  ],
})
export class AppModule {}

Class providers useClass [Custom Provider]

The useClass syntax allows you to dynamically determine a class that a token should resolve to. For example, suppose we have an abstract (or default) ConfigService class

const configServiceProvider = {
  provide: ConfigService,
  useClass:
    process.env.NODE_ENV === 'development'
      ? DevelopmentConfigService
      : ProductionConfigService,
};

@Module({
  providers: [configServiceProvider],
})
export class AppModule {}

Factory providers: useFactory [Custom Provider]

The useFactory syntax allows for creating providers dynamically. The actual provider will be supplied by the value returned from a factory function

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider, optionalProvider?: string) => {
    return 'something'
  },
  inject: []
};

@Module({
  providers: [
    connectionFactory,
    OptionsProvider,
  ],
})
export class AppModule {}

Alias providers: useExisting [Custom Provider]

The useExisting syntax allows you to create aliases for existing providers. This creates two ways to access the same provider. In the example below, the (string-based) token 'AliasedLoggerService' is an alias for the (class-based) token LoggerService. Assume we have two different dependencies, one for 'AliasedLoggerService' and one for LoggerService. If both dependencies are specified with SINGLETON scope, they'll both resolve to the same instance.

@Injectable()
class LoggerService {
  /* implementation details */
}

const loggerAliasProvider = {
  provide: 'AliasedLoggerService',
  useExisting: LoggerService,
};

@Module({
  providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}

In this blog post, we have explored the powerful concepts of dependency injection and custom providers in Nest JS. We have seen how Nest JS simplifies the process of dependency injection, allowing us to build scalable and modular applications with ease.

Through practical code examples, we have learned how to use the built-in providers and decorators provided by Nest JS, such as @Injectable, @Inject, and @Optional, to effortlessly inject dependencies into our application. We have also delved into the creation of custom providers, including class providers, value providers, and factory providers, and discovered how they enable us to define our own dependencies and inject them into our Nest JS application.

Furthermore, we have explored the usage of dependency injection in controllers, services, middleware, and interceptors, understanding how it improves modularity and testability in our Nest JS projects. We have discussed best practices for handling circular dependencies, advanced techniques like hierarchical injectors and conditional injection, and integration of external dependencies using dependency injection.

Unit testing has also been a focal point, as we have seen how dependency injection simplifies the process of writing effective unit tests using Nest JS testing utilities. Additionally, we have considered performance considerations and provided tips for optimizing dependency resolution and instantiation.

By leveraging the knowledge and examples shared in this blog post, you now have the necessary tools to implement dependency injection and custom providers in your Nest JS applications. This will empower you to build robust, scalable, and maintainable software solutions.

We encourage you to continue exploring and experimenting with dependency injection in your own projects. By embracing the power of dependency injection in Nest JS, you can unlock the full potential of the framework and create exceptional applications.

https://www.youtube.com/watch?v=ODFOFS3bA4U&list=PLIGDNOJWiL1-8hpXEDlD1UrphjmZ9aMT1&index=5

Comments