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
- Achieving dependency injection with containers
- Using TypeDI to achieve dependency injection in Node.js
- Starter project
- Other benefits of TypeDI
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?
- The request will come to the
Controller
, which handles all the routing - The
Controller
will call theService
, which handles all the business logic - The
Service
will call theRepository
, 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 Token
s 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