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