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:
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
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.
UserRepositoryWe 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 Tokens 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!
Monitor failed and slow network requests in productionDeploying 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 lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
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.

Vibe coding isn’t just AI-assisted chaos. Here’s how to avoid insecure, unreadable code and turn your “vibes” into real developer productivity.

GitHub SpecKit brings structure to AI-assisted coding with a spec-driven workflow. Learn how to build a consistent, React-based project guided by clear specs and plans.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.
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 now