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 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:
So, it’s clear why dependency injection is a good thing for your application, but how can we do it?
Controller
, which handles all the routingController
will call the Service
, which handles all the business logicService
will call the Repository
, which handles all the database callsUserController -> 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:
So we need a way to inject the instance of UserRepository
into the UserService
class. This is where dependency injection comes into play.
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.
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.
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.
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.
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.
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.
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?
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!
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.