NestJS Dependency Injection - Decoupling Services With Interfaces

Published on

I recently answered a question on StackOverflow pertaining to decoupling services from controllers in NestJS with interfaces. I’ve seen this accomplished with both interfaces and abstract classes but will be focusing on interface for this post.

If you’ve worked with in .NET or Java/Spring, you’ll know this has become very common practice when building systems while some might consider it over engineering. Regardless we’ll demonstrate how to register your services with the NestJS dependency injector to allow for injection to interface types.

 

Creating Our Initial Project

Lets quickly generate a new NestJS project and create a greeting module, controller, and two different services. The two difference services will implement a common interface and we’ll see how we can use interfaces with the dependency injector to decouple our service implementations from our controller.

nest new nestjs-di-decoupling-with-interfaces
nest generate module greeting
nest generate controller greeting
nest generate service greeting/personal-greeting --flat
nest generate service greeting/professional-greeting --flat
touch src/greeting/greeting-service.interface.ts

 

Creating Our Greeting Services

We will start by defining our greeting service interface with a single abstract greet() method.

export const GREETING_SERVICE = 'GREETING SERVICE';

export interface IGreetingService {
  greet(name: string): Promise<string>;
}

One thing you’ll notice is a GREETING_SERVICE constant. Since JavaScript doesn’t support/understand interfaces, when we compile down our TypeScript to JavaScript our interfaces no longer exist.

The dependecy injector will try to resolve dependencies by type which works fine for classes since classes exist in JavaScrtipt but runs into issues with interfaces because interfaces don’t exist in javascript so it can’t determine what to inject.

To use dependency injection with interfaces we need to create a token and register that token with an implementation/class. We then can use that token to tell the injector what to inject when defining a dependency as an interface type.

Now lets create two different implementation of our interface, PersonalGreetingService and ProfessionalGreetingService.

Our PersonalGreetingService

import { Injectable } from '@nestjs/common';
import { IGreetingService } from './greeting-service.interface';

@Injectable()
export class PersonalGreetingService implements IGreetingService {
  public async greet(name: string): Promise<string> {
    return `Hey, how's it goin' ${name}?`;
  }
}

And our ProfessionalGreetingService

import { Injectable } from '@nestjs/common';
import { IGreetingService } from './greeting-service.interface';

@Injectable()
export class ProfessionalGreetingService implements IGreetingService {
  public async greet(name: string): Promise<string> {
    return `Hello ${name}, how are you today?`;
  }
}

 

Registering Our Service As A Provider

So you’re probably wondering where the token comes into play. We use our token when registering our service with our module and supply it to the @Inject decorator when we want to inject our implementation. Lets update our GreetingModule to define which implementation to associate with our token…

import { Module } from '@nestjs/common';
import { ProfessionalGreetingService } from './services/professional-greeting.service';
import { PersonalGreetingService } from './services/personal-greeting.service';
import { GREETING_SERVICE } from './services/greeting-service.interface';
import { GreetingController } from './controllers/greeting.controller';

@Module({
  providers: [
    {
      // You can switch useClass to different implementation
      useClass: ProfessionalGreetingService,
      provide: GREETING_SERVICE
    }
  ],
  controllers: [
    GreetingController
  ]
})
export class GreetingModule {}

 

Injecting Our Service

In our module we registered the ProfessionalGreetingService class with our GREETING_SERVICE token. Now when we want to inject our greeting service we can use the @Inject decorator and provide our token so that the dependency injector knows what implementation to inject. This will allow us to declare our constructor arguments as interface types.

Let update our controller to inject our ProfessionalGreetingService as an interface type.


import { Controller, Get, Inject, Query } from '@nestjs/common';
import { GREETING_SERVICE, IGreetingService } from '../services/greeting-service.interface';

@Controller('greeting')
export class GreetingController {
  constructor(
    @Inject(GREETING_SERVICE)
    private readonly _greetingService: IGreetingService
  ) {}

  @Get()
  public async getGreeting(@Query('name') name: string): Promise<string> {
    return await this._greetingService.greet(name || 'John');
  }
}

The @Inject decorator takes our GREETING_SERVICE token which will give us an instance of ProfessionalGreetingService. Our _greetingService is typed as an IGreetingService which decouples our implementation from our controller.

We can run our project with npm start and navigate to http://localhost:3000/greeting and we will get our professional greeting response. Likewise if we swap out our ProfessionalGreetingService in our GreetingModule for the PersonalGreetingService we will then get our personal greeting response.

 

Final Github Repository

 

comments powered by Disqus