This is a premium alert message you can set from Layout! Get Now!

Dependency injection in Node.js with TypeDI

0

Dependency injection is an essential concept in object-oriented programming. It is a way to decouple the creation of objects from their usage. In this article, we will learn what dependency injection is and how we can use it in Node.js applications using the TypeDI library.

To jump ahead:

Dependency injection

Dependency injection is a design pattern that allows us to inject dependencies into a class instead of creating the dependency instance inside the class.

Dependency injection can help us:

  • Write flexible classes
  • Easily test our code
  • Reduce the amount of boilerplate code
  • Improve the readability of our code

So, it’s clear why dependency injection is a good thing for your application, but how can we do it?

First, let’s look at what pain point dependency injections solve.
Think of a standard API that returns a list of users. The request flow has three steps:
  1. The request will come to the Controller, which handles all the routing
  2. The Controller will call the Service, which handles all the business logic
  3. The Service will call the Repository, which handles all the database calls
UserController -> UserService -> UserRepository

So the Controller depends on the Service, and the Service depends on the Repository. This is a typical dependency flow in a Node.js application.

If we look at the code, we can see that the Controller creates the instance of UserService, and the Service creates the instance of Repository.

The service class will look something like this:

import { UserRepository } from "./UserRepository";

export class UserService {
  userRepo: UserRepository;

  constructor() {
    this.userRepo = new UserRepository();
  }

  getUserData = () => {
    this.userRepo.getAll();
  };
}

And the controller will look something like this:

import { UserService } from "./UserService";

export class UserController {
  userService: UserService;

  constructor() {
    this.userService = new UserService();
  }

  getUserData = () => {
    this.userService.getUserData();
  };
}

Now, if we want to use the UserService class, we will have to create the instance of UserRepository inside the UserService class.

Creating one class instance inside another is not a good practice because now, these two classes (i.e., UserRepository and UserService) have tight coupling.

Say we want to test our UserService class. Do we want our test code to interact with the actual database?

No — we want to mock the database calls and test our UserService class. Otherwise:

  • We will have to create a test database
  • Our test suite will depend on the database. So if something breaks in the database, your test suite will also break
  • The test suite will be very slow

So we need a way to inject the instance of UserRepository into the UserService class. This is where dependency injection comes into play.

Achieving dependency injection with containers

The most common way to achieve dependency injection is to use a dependency injection container.

We can create a global container object that will hold all the instances of the dependencies, and we can inject the dependencies into the class.

The most common way to inject the dependencies is to use the constructor. We can use the constructor of our UserService class to inject the instance of the UserRepository class.

Our UserService class will look something like this:

import { UserRepository } from "./UserRepository";

export class UserService {
  userRepo: UserRepository;

  constructor(userRepo: UserRepository) {
    this.userRepo = userRepo;
  }

  getUserData = () => {
    this.userRepo.getAll();
  };
}

Now we can pass the instance of UserRepository to the UserService class. And guess what?

When we are testing the UserService class, we can pass the mock instance of UserRepository to the UserService class and test it:

import { UserService } from "./UserService";
import { UserRepository } from "./UserRepository";

const mockUserRepo = {
  getAll: jest.fn(),
};

const userService = new UserService(mockUserRepo);

userService.getUserData();

expect(mockUserRepo.getAll).toHaveBeenCalled();

We still have to create the instance of the UserRepository class and inject it into the UserService class, which we’ll have to do whenever we want to use the UserService class. But we don’t want to do this every time — just once.

Let’s see how we can achieve this.

Using TypeDI to achieve dependency injection in Node.js

There are multiple ways to achieve dependency injection in Node.js. We can create our dependency container, create the instances ourselves, or inject them into the runtime.

But there is a better way to achieve dependency injection in Node. It’s by using a library called TypeDI, which supports multiple DI containers, is very flexible and speedy, and is straightforward to use.

There are some other popular options for dependency injection, like inversify and awilix, but I found TypeDI to be much cleaner than the others.

Starter project

You can skip this step if you already have an existing Express project. Otherwise, you can build a boilerplate project with Express.js and TypeScript using the following command.

git clone https://github.com/Mohammad-Faisal/express-typescript-skeleton-boilerplate

To get started, let’s first install the dependency inside our Node application.

npm install typedi reflect-metadata

Then, modify our tsconfig.json file to properly work with the typedi. Add the following three options under the compilerOptions:

"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false // this one is for preventing the typescript 
                                         errors while using @Inject()

Now, import reflect-metadata at the beginning of our application, like the following, inside the index.ts file:

import "reflect-metadata";

This will solve the reflect-metadata shim is required when using class decorators error.

There are multiple ways to use TypeDI based on the use case. Let’s see a few of them.

Get from a global container

We can get the instance of UserRepository from the global container. This is the direct use of TypeDI:

import { UserRepository } from "./UserRepository";
import { Service, Inject, Container } from "typedi";

@Service()
export class UserService {
  getUserData = () => {
    const userRepo = Container.get(UserRepository);
    userRepo.getAll();
  };
}

But you must mark the UserRepository class with the @Service() decorator. Otherwise, you will get an error:

import { Service } from "typedi";

@Service()
export class UserRepository {
  getAll = () => {
    console.log("Getting all the users");
  };
}

You may wonder why we are using the @Service() decorator here.

The @Service() decorator is used to register the UserRepository as a service in the global container so that we can get the instance of UserRepository from the global container.

Now, when we call the Container.get(UserRepository), it will return the instance of the UserRepository class.

Inject the instance of UserRepository

We can also inject the instance of UserRepository into the UserService class using the @Inject() decorator:

import { UserRepository } from "./UserRepository";
import { Service, Inject, Container } from "typedi";

@Service()
export class UserService {
  @Inject()   // <- notice here
  userRepo: UserRepository;

  getUserData = () => {
    this.userRepo.getAll();
  };
}

Now we don’t have to use the Container.get(UserRepository) to get the instance of the UserRepository class. We can directly use the this.userRepo to access the instance of the UserRepository class.

Inject the dependency using the constructor

We can inject the dependency using the constructor of the class:

import { UserRepository } from "./UserRepository";
import { Service, Inject } from "typedi";

@Service()
export class UserService {
  userRepo: UserRepository;

  constructor(@Inject() userRepo: UserRepository) {
    this.userRepo = userRepo;
  }

  logUserData = () => {
    this.userRepo.someFunction();
  };
}

This is a very common way to inject dependency into the class. It follows the dependency injection pattern.

And that’s how you implement the dependency injection in Node.js using TypeDI.

Other benefits of TypeDI

There are other benefits of using the TypeDI library. We can use the Container class to set global variables across the application.

First, we must set the variable we need to access across the application:

import 'reflect-metadata';
import { Container, Token } from 'typedi';

export const SOME_GLOBAL_CONFIG_VALUE = new Token<string>('SOME_CONFIG');
Container.set(SOME_GLOBAL_CONFIG_VALUE, 'very-secret-value');

Now, if we need this value anywhere in the application, we can use the following piece of code:

import { Container, Token } from 'typedi';

const MY_SECRET = Container.get(SOME_GLOBAL_CONFIG_VALUE);

This is also very type-safe, because the Tokens are typed.

How cool is that?

Conclusion

Thank you for reading this far. Today I demonstrated what dependency injection is in the context of a Node.js application. We also learned to use the TypeDI library to achieve dependency injection in a practical project. For more information, check out the TypeDI documentation and the GitHub repository for this project.

I hope you learned something new today. Have a great rest of your day!

The post Dependency injection in Node.js with TypeDI appeared first on LogRocket Blog.



from LogRocket Blog https://ift.tt/xQZJ2Kr
Gain $200 in a week
via Read more

Post a Comment

0 Comments
* Please Don't Spam Here. All the Comments are Reviewed by Admin.
Post a Comment

Search This Blog

To Top