Implementing Dependency Inversion Principle in Nest.js using Abstract Classes
Tags: nestjs, typescript, solid
Published: 2024-04-06 15:38:13

---
title: Implementing Dependency Inversion Principle in Nest.js using Abstract Classes
published: true
description: 
tags: nestjs, typescript, solid
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3cyock95hq87lln4okdo.png
# Use a ratio of 100:42 for best results.
# published_at: 2024-04-06 12:01 +0000
---

### What is Dependency Inversion?

The Dependency Inversion Principle (DIP) is one of the five SOLID principles. The principle says *High-level modules (classes) should not depend on low-level modules. Both should depend on abstractions.*


Let's start with an example that does not use this principle. I created a UserService class that is in charge of creating new users.

```ts
class UsersService {

    public createUser(data: User) {
        // ...
    }

}
```
However, we need to add logs as well. Luckily, the is a LoggerService that does the logging for the whole application. Let's apply it:

```ts
class UsersService {

    private logger;

    public createUser(data: User) {
        logger = new LoggerService();

        loggger.log('Started creating users...');

        // ...
    }

}
```
The logger is in place and it does the job, but, there are a few problems:

#### 1. Difficult to maintain
What if the implementation of LoggerService changes? For example, new methods are added, existing ones are changed, and there is a parameter added to the constructor of a LoggerService. If that happens, we'd need to go to all places where Logger Service is used and make the necessary changes:

```ts
logger = new LoggerService('parameter');
```

#### 2. Hard to swap dependencies
What if sometime in the future we need to swap the existing LoggerService that logs in the console with another that logs in the database? Or if we have a Payment class that uses PayPal and we need to swap it with another class that uses Stripe?

In either case, a huge refactor is unavoidable.

#### 3. Hard to Test
Because the UserService is directly calling the logger, it means that when testing the service, we'd make actual logs to the console/database and we don't want that. Imagine if this was an Email Service and it sent emails to customers while running tests on each build. It'd be very expensive for us and we'd also spam the customers.


Enter **Dependency Inversion**. Instead of invoking LoggerService directly, the UserService will call its **interface**. This pattern solves all previously mentioned problems, as we're not bound to work with actual classes but rather their **abstractions**.

The abstraction will have the methods available on the LoggerService and in case the underlying implementation or framework changes, it won't matter.

## Implementing Dependency Inversion in Nest.js

Now we're going to apply this principle in Nest.js.

### Quick Note

Nest.js however does **not** allow using interfaces as providers because [injection tokens](https://www.c-sharpcorner.com/article/understanding-angular-injection-tokens/) only work strings, symbols, and classes ([as explained here](https://stackoverflow.com/a/53040617/7584952)). However, it does work with Abstract classes which we'll use here.

### App overview

```
📁 src 
|__ 📁 models
|_____ loggerService.class.ts
|__ 📁 services
|_____ 📁 ConsoleLogger
|_______ consolelogger.service.ts
|_____ 📁 FileLogger
|_______ filelogger.service.ts
|__ app.module.ts 
|__ app.controller.ts 
```

### Generate abstract class

```ts
// ./models/loggerservice.class.ts
export default abstract class LoggerService {
  abstract log(data: string): void;
}
```

### Generate Two Services 

* `ConsoleLoggerService` - Implementation of LoggerService that logs to the console 
* `FileLoggerService` - Implementation of LoggerService that logs to the file

Each service extends the abstract `LoggerService` class and implements the `log()` method.

```ts
// ./services/ConsoleLogger/consolelogger.service.ts
import { Injectable } from '@nestjs/common';
import LoggerService from '../../models/loggerservice.class';

@Injectable()
export class ConsoleLoggerService extends LoggerService {
  log(data: string): void {
    console.log(data);
  }
}
```

```ts
// ./services/FileLogger/filelogger.service.ts
import { Injectable } from '@nestjs/common';
import LoggerService from '../../models/loggerservice.class';
// Node.js built-in File-System API
import { createWriteStream } from 'fs';


@Injectable()
export class FileLoggerService extends LoggerService {
  log(data: string): string {

    const stream = createWriteStream('<your-file>.log', { flags: 'a' });
  // 'a' means append to the existing file

  stream
    .end(data) // insert data into file
    .on('error', () => console.log('Unable to log to file!'));
  }
}
```

### Setup Appmodule

In AppModule we'll inject our services using the abstract class as a provider token.

```ts
import LoggerService from './models/loggerservice.class';
import { ConsoleLoggerService } from './services/ConsoleLogger/consolelogger.service';
import { FileLoggerService } from './services/FileLogger/filelogger.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: LoggerService, // our custom injection token
      useClass: /* either implementation of LoggerService */
    },
  ],
})
export class AppModule {}
```


### Setup AppController

```ts
import { Controller, Get, Inject } from '@nestjs/common';
import LoggerService from './models/loggerservice.class';

@Controller()
export class AppController {
  constructor(private readonly logger: LoggerService) {}

  @Get()
  getHello(): string {
    this.logger.log('Greeting sent to the user');
    return 'Hello World!';
  }
}
```

The beauty here is that the consumer has no clue what logger service is used.

## Let's give it a whirl!

Set a ConsoleLoggerService inside the AppModule and run the app:

```ts
  providers: [
    AppService,
    {
      provide: LoggerService, // our custom injection token
      useClass: ConsoleLoggerService
    },
  ],
```

```bash
npm run start:dev
```

```bash
curl localhost:3000
```

The console output should be visible in the terminal.

![console-logger](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8ci1q05szke3g67yzdvs.png)

Now to the same for the FileLoggerService. Important note - remember to create your log file first, e.g. access.log.

```ts
  providers: [
    AppService,
    {
      provide: LoggerService, // our custom injection token
      useClass: FileLoggerService
    },
  ],
```

```bash
curl localhost:3000
```

The information is logged into the file as expected.

![file-logger](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vanwbtwipv6t3dsukcye.png)

And that's it!
If you'd like to learn more about logging in Node.js, be sure to check out my blog on [Automated Logging in Express.js](https://mirzaleka.medium.com/automated-logging-in-express-js-a1f85ca6c5cd). For everything else awesome, follow me on [Dev.to](https://dev.to/mirzaleka) and on [Twitter](https://twitter.com/mirzaleka) to stay up to date with my content updates.