Dependency Injection in NestJs for beginners

Dependency Injection in NestJs for beginners

·

6 min read

Introduction

Hey there, fellow developers! Today, I want to share with you a powerful technique that can take your NestJS applications to a whole new level. It's called dependency injection (DI), and trust me, once you get the hang of it, it'll become your secret weapon for writing clean, maintainable, and modular code. So buckle up and get ready to dive into the wonderful world of dependency injection in NestJS!

Unleashing the Power of Dependency Injection

Okay, let's break it down. Dependency injection is like having a super-smart assistant who automatically handles all the nitty-gritty stuff for you. It's a software design pattern that lets you decouple components from their dependencies, making your code more flexible and scalable.

Picture this: you're building an awesome NestJS app that sends SMS notifications to users. But hey, what if you want to switch from one SMS provider to another without breaking a sweat? That's where DI comes in to save the day! With dependency injection, you can seamlessly swap out providers just by updating the module where it's injected. No more headaches or messy code changes. How cool is that? - We will get to this example as soon as we knock down the constructor injection which is one of the techniques that powers dependency injection.

Simplifying Your Life with Constructor Injection

Now, let's talk about constructor injection, a key aspect of DI. Imagine you have a complex service in your app that requires multiple dependencies to function properly. Without dependency injection, you would have to manually create and manage instances of each dependency within the service. It can quickly become a nightmare, especially when the number of dependencies increases.

But fear not! Dependency injection comes to the rescue. With constructor injection, you can simply declare the dependencies as constructor parameters, and NestJS injects the required instances for you. It's like having a magical genie fulfilling your service's every wish.

Imagine we have a ComplexService that needs three dependencies: DependencyA, DependencyB, and DependencyC. Now, let's say DependencyA itself requires some constructor values. With dependency injection, you don't need to worry about providing those values explicitly. Here's an example:

import { Injectable } from '@nestjs/common';
import { DependencyA } from './dependency-a';
import { DependencyB } from './dependency-b';
import { DependencyC } from './dependency-c';

@Injectable()
export class ComplexService {
  constructor(
    private readonly dependencyA: DependencyA,
    private readonly dependencyB: DependencyB,
    private readonly dependencyC: DependencyC,
  ) {
    // Constructor logic goes here
  }

  // Service methods and functionality
}

In this example, even if DependencyA requires some constructor values, you don't need to pass them explicitly when creating an instance of ComplexService. NestJS will automatically handle the injection of all dependencies, including the required constructor values of DependencyA.

What If There's No Dependency Injection?

Now, imagine a scenario where you don't use dependency injection. Without DI, you would have to manually create instances of each dependency within the service. Your code might look like this:

import { Injectable } from '@nestjs/common';
import { DependencyA } from './dependency-a';
import { DependencyB } from './dependency-b';
import { DependencyC } from './dependency-c';

@Injectable()
export class ComplexService {
  private readonly dependencyA: DependencyA;
  private readonly dependencyB: DependencyB;
  private readonly dependencyC: DependencyC;

  constructor() {
    this.dependencyA = new DependencyA(/* Constructor values for DependencyA */);
    this.dependencyB = new DependencyB();
    this.dependencyC = new DependencyC();
    // Constructor logic goes here
  }

  // Service methods and functionality
}

As you can see, without dependency injection, you need to manually create and manage instances of each dependency, including providing the required constructor values. This can quickly become cumbersome and error-prone, especially as your application grows in complexity.

Switching Providers (Dependency Service) with Ease

Let's dive into a practical example to understand how dependency injection can make our lives easier.

Imagine we have an SmsService responsible for sending SMS notifications to users, and it needs a way to interact with different SMS providers, such as Twilio or Nexmo.

To achieve this flexibility, we use a concept called dependency injection. It allows us to decouple the SmsService from any specific SMS provider implementation. But how does it work?

In our scenario, we define an interface called ISmsProvider. An interface is like a contract that specifies a set of methods that a class must implement. In our case, the ISmsProvider interface defines a method called sendSMS that takes in a phone number and a message and returns a boolean indicating whether the SMS was successfully sent.

Here's an example of how the ISmsProvider interface could look:

interface ISmsProvider {
  sendSMS(phoneNumber: string, message: string): boolean;
}

By defining this interface, we're essentially saying that any class that wants to be an SMS provider must implement the sendSMS method with the same parameters and return type.

Now, let's move on to the SmsService. Instead of directly using a specific provider like Twilio or Nexmo, we declare a constructor parameter of type ISmsProvider in the SmsService class. This means that the SmsService expects an object that conforms to the ISmsProvider interface to be injected when creating an instance of the SmsService.

By injecting the ISmsProvider through the constructor, we create a loose coupling between the SmsService and the actual SMS provider implementation. This allows us to switch providers easily without modifying the core logic of the SmsService.

Let's see an example:

class SmsService {
  constructor(private readonly smsProvider: ISmsProvider) {}

  sendSMS(phoneNumber: string, message: string): boolean {
    return this.smsProvider.sendSMS(phoneNumber, message);
  }
}

In this example, the SmsService class has a method called sendSMS that takes a phone number and a message as parameters. It uses the injected smsProvider object to call the sendSMS method on the provider, abstracting away the specific implementation details.

Here's an example of the TwilioSmsProvider:

import { Injectable } from '@nestjs/common';
import { ISmsProvider } from './sms.provider.interface';

@Injectable()
export class TwilioSmsProvider implements ISmsProvider {
  sendSMS(phoneNumber: string, message: string): Promise<boolean> {
    // Implement the Twilio SMS sending logic here
  }
}

To switch providers, all we need to do is update the module configuration where the SmsService is injected. Here's an example:

import { Module } from '@nestjs/common';
import { SmsService } from './sms.service';
import { ISmsProvider } from './sms.provider.interface';
import { TwilioSmsProvider } from './twilio-sms.provider';

@Module({
  providers: [
    {
      provide: ISmsProvider,
      useClass: TwilioSmsProvider,
    },
    SmsService,
  ],
})
export class SmsModule {}

In the SmsModule configuration, we define that the ISmsProvider should be provided by the TwilioSmsProvider class. By simply updating this configuration, we can seamlessly switch to using Twilio as our SMS provider.

The beauty of dependency injection is that we don't need to touch the SmsService implementation. It remains blissfully unaware of the underlying provider implementation. This decoupling allows us to easily switch providers, improve flexibility, and keep our codebase clean and modular.

Testability? You Bet!

But wait, there's more! Dependency injection also supercharges the testability of your code. By injecting mock or stub implementations of dependencies during unit tests, you can isolate your components and ensure reliable and thorough testing. No more tangled web of dependencies ruining your testing party. It's all about that sweet, sweet isolation!

Conclusion

Congratulations, my fellow developer! You're now armed with the knowledge of dependency injection in NestJS. With its modularity, scalability, and testability superpowers, you can build remarkable applications that are a breeze to maintain and extend.

So, don't be shy—embrace dependency injection, unlock the full potential of NestJS, and watch your codebase soar to new heights. Happy coding, and may the DI force be with you!