Dependency Injection & Inversion Explained | Node.js w/ TypeScript


This topic is taken from Solid Book - The Software Architecture & Design Handbook w/ TypeScript + Node.js. Check it out if you like this post.
Translated by readers to: Brazilian Portuguese
One of the first things we learn in programming is to decompose large problems into smaller parts. That divide-and-conquer approach can help us to assign tasks to others, reduce anxiety by focusing on one thing at a time, and improve modularity of our designs.
But there comes a time when things are ready to be hooked up.
That's where most developers go about things the wrong way.
Most developers that haven't yet learned about the solid principles or software composition, and proceed to write tightly couple modules and classes that shouldn't be coupled, resulting in code that's hard to change and hard to test.
In this article, we're going to learn about:
- Components & software composition
- How NOT to hook up components
- How and why to inject dependencies using Dependency Injection
- How to apply Dependency Inversion and write testable code
- Considerations using Inversion of Control containers
Terminology
Let's make sure that we understand the terminology on wiring up dependencies before we continue.
Components
I'm going to use the term component a lot. That term might strike a chord with React.js or Angular developers, but it can be used beyond the scope of web, Angular, or React.
A component is simply a part of an application. It's any group of software that's intended to be a part of a larger system.
The idea is to break a large application up into several modular components that can be independently developed and assembled.
The more you learn about software, the more you realize that good software design is all about composition of components.
Failure to get this right leads to clumpy code that can't be tested.
Dependency Injection
Eventually, we'll need to hook components up somehow. Let's look at a trivial (and non-ideal) way that we might hook two components up together.
In the following example, we want to hook up a UserController
so that it can retrieve all the User[]
s from a UserRepo
(repository) when someone makes an HTTP GET request to /api/users
.
/**
* @class UserRepo
* @desc Responsible for pulling users from persistence.
**/
export class UserRepo {
constructor () {}
getUsers (): Promise<User[]> {
// Use Sequelize or TypeORM to retrieve the users from
// a database.
}
}
And the controller...
import { UserRepo } from '../repos' // Bad
/**
* @class UserController
* @desc Responsible for handling API requests for the
* /user route.
**/
class UserController {
private userRepo: UserRepo;
constructor () {
this.userRepo = new UserRepo(); // Also bad, read on for why
}
async handleGetUsers (req, res): Promise<void> {
const users = await this.userRepo.getUsers();
return res.status(200).json({ users });
}
}
In the example, we connected a UserRepo
directly to a UserController
by referencing the name of the UserRepo
class from within the UserController
class.
This isn't ideal. When we do that, we create a source code dependency.
Source code dependency: When the current component (class, module, etc) relies on at least one other component in order to be compiled. Source code depdendencies should be limited.
The problem is that everytime that we want to spin up a UserController
, we need to make sure that the UserRepo
is also within reach so that the code can compile.
The UserController class depends directly on the UserRepo class.
When might you want to spin up an isolated UserController
?
During testing.
It's a common practice during testing to mock or fake dependencies of the current module under test in order to isolate and test different behaviors.
Notice how we're a) importing the concrete UserRepo
class into the file and b) creating an instance of it from within the UserController
constructor?
That renders this code untestable. Or at least, if UserRepo
was connected to a real live running database, we'd have to bring the entire database connection with us to run our tests, making them very slow...
Dependency Injection is a technique that can improve the testability of our code.
It works by passing in (usually via constructor) the dependencies that your module needs to operate.
If we change the way we inject the UserRepo
from UserController
, we can improve it slightly.
import { UserRepo } from '../repos' // Still bad
/**
* @class UserController
* @desc Responsible for handling API requests for the
* /user route.
**/
class UserController {
private userRepo: UserRepo;
constructor (userRepo: UserRepo) { // Better, inject via constructor
this.userRepo = userRepo;
}
async handleGetUsers (req, res): Promise<void> {
const users = await this.userRepo.getUsers();
return res.status(200).json({ users });
}
}
Even though we're using dependency injection, there's still a problem.
UserController
still relies on UserRepo
directly.
This dependency relationship still holds true.
Even still, if we wanted to mock out our UserRepo
that connects to a real SQL database for a mock in-memory repository, it's not currently possible.
UserController
needs a UserRepo
, specifically.
let userController: UserController;
beforeEach(() => {
userController = new UserController(
new UserRepo() // Slows down tests, needs a db running
)
});
So.. what do we do?
Introducing the Dependency Inversion Principle!
Dependency Inversion
Dependency Inversion is a technique that allows us to decouple components from one another. Check this out.
What direction does the flow of dependencies go in right now?
From left to right. The UserController
relies on the UserRepo
.
OK. Ready?
Watch what happens when we slap an interface in between the two components make UserRepo
implement an IUserRepo
interface, and then point the UserController
to refer to that instead of the UserRepo
concrete class.
/**
* @interface IUserRepo
* @desc Responsible for pulling users from persistence.
**/
export interface IUserRepo { // Exported
getUsers (): Promise<User[]>
}
class UserRepo implements IUserRepo { // Not exported
constructor () {}
getUsers (): Promise<User[]> {
...
}
}
And update the controller to refer to the IUserRepo
interface instead of the UserRepo
concrete class.
import { IUserRepo } from '../repos' // Good!
/**
* @class UserController
* @desc Responsible for handling API requests for the
* /user route.
**/
class UserController {
private userRepo: IUserRepo; // like here
constructor (userRepo: IUserRepo) { // and here
this.userRepo = userRepo;
}
async handleGetUsers (req, res): Promise<void> {
const users = await this.userRepo.getUsers();
return res.status(200).json({ users });
}
}
Now look at direction of the flow of dependencies.
You see what we just did? By changing all of the references from concrete classes to interfaces, we've just flipped the dependency graph and created an architectural boundary inbetween the two components.
Design principle: Program against interfaces, not implementations.
Maybe you're not as excited about this as I am. Let me show you why this is so great.

And if you like this article so far, you might like my book, "Solid - The Software Architecture & Design Handbook w/ TypeScript + Node.js". You'll learn how to write testable, flexible, and maintainable code using principles (like this one) that I think all professional people in software should know about. Check it out.
Remember when I said that we wanted to be able to run tests on the UserController
without having to pass in a UserRepo
, solely because it would make the tests slow(UserRepo
needs a db connection to run)?
Well, now we can write a MockUserRepo
which implements IUserRepo
and all the methods on the interface, and instead of using a class that relies on a slow db connection, use a class that contains an internal array of User[]
s (much quicker! ⚡).
That's what we'll pass that into the UserController
instead.
Using a MockUserRepo
to mock out our UserController
import { IUserRepo } from '../repos';
class MockUserRepo implements IUserRepo {
private users: User[] = [];
constructor () {}
async getUsers (): Promise<User[]> {
return this.users;
}
}
Tip: Adding "async" to a method auto-wraps it in a Promise, making it easy to fake asynchronous activity.
We can write a test using a testing framework like Jest.
import { MockUserRepo } from '../repos/mock/mockUserRepo';
let userController: UserController;
const mockResponse = () => {
const res = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
return res;
};
beforeEach(() => {
userController = new UserController(
new MockUserRepo() // Speedy! And valid since it inherits IUserRepo.
)
});
test ("Should 200 with an empty array of users", async () => {
let res = mockResponse();
await userController.handleGetUsers(null, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ users: [] });
})
Congrats. You (more or less) just learned how write testable code!.
The primary wins of DI
Not only does this decoupling make your code testable, but it improves the following characteristics of your code:
- Testability: We can substitute expensive to infrastructure components for mock ones during testing.
- Substitutability: If we program against an interface, we enable a plugin architecture adhering to the Liskov Substitution Principle, which makes it incredibly easy for us to swap out valid plugins, and program against code that doesn't yet exist. Because the interface defines the shape of the dependency, all we need to do to substitute the current dependency is create a new one that adheres to the contract defined by the interface. See this article to dive deeper on that.
- Flexibility: Adhering to the Open Closed Principle, a system should be open for extension but closed for modification. That means if we want to extend the system, we need only create a new plugin in order to extend the current behavior.
- Delegation: Inversion of Control is the phenomenon we observe when we delegate behavior to be implemented by someone else, but provide the hooks/plugins/callbacks to do so. We design the current component to invert control to another one. Lots of web frameworks are built on this principle.
Inversion of Control & IoC Containers
Applications get much larger than just two components.
Not only do we need to ensure we're referring to interfaces and NOT concrete implementations, but we also need to handle the process of manually injecting instances of dependencies at runtime.
If your app is relatively small or you've got a style guide for hooking up dependencies on your team, you could do this manually.
If you've got a huge app and you don't have a plan for how you'll accomplish dependency injection within in your app, it has potential to get out of hand.
It's for that reason that Inversion of Control (IoC) Containers exist.
They work by requiring you to:
- Create a container (that will hold all of your app dependencies)
- Make that dependency known to the container (specify that it is injectable)
- Resolve the depdendencies that you need by asking the container to inject them
Some of the more popular ones for JavaScript/TypeScript are Awilix and InversifyJS.
Personally, I'm not a huge fan of them and the additional infrastructure-specific framework logic that they scatter all across my codebase.
If you're like me and you're not into container life, I have my own style guide for injecting dependencies that I talk about in solidbook.io. I'm also working on some video content, so stay tuned!
Inversion of Control: Traditional control flow for a program is when the program only does what we tell it to do (today). Inversion of control flow happens when we develop frameworks or only refer to plugin architecture with areas of code that can be hooked into. In these areas, we might not know (today) how we want it to be used, or we wish to enable developers to add additional functionality. That means that every lifecycle hook in React.js or Angular is a good example of Inversion of Control in practice. IoC is also often explained by the "Hollywood Design Principle": Don't call us, we'll call you.
Discussion
Liked this? Sing it loud and proud 👨🎤.
Stay in touch!
Enjoying so far? Join 15000+ Software Essentialists getting my posts delivered straight to your inbox each week. I won't spam ya. 🖖
View more in Software Design
You may also enjoy...
A few more related articles




Want to be notified when new content comes out?
Join 15000+ other Software Essentialists learning how to master The Essentials of software design and architecture.
14 Comments
Commenting has been disabled for now. To ask questions and discuss this post, join the community.
Here's a simple to use and lightweight option for dependency injection with Typescript in Node.js 8+: https://www.npmjs.com/package/@sailplane/injector
wow thats super cool! Amazing work Khalil. Keep it up I really enjoy your articles and soon will get your book! Amazing!
Certainly the article I was looking for to actually understand how Dependency injection and Dependency Inversion are useful at all.
Man you rock, I think I might end up buying your book. Been through some of the nightmares you described in an app I developed last year.
W00t :)
Great article and in depth explanation!
I am missing a bit on how the controller is actually called within the context of an application. What i am still looking for is someone to translate the clean architecture approach of Uncle Bob (Robert C. Martin) into JS / Typescript and shows it on an api style app and maybe a react app.
@Khalil Stemmler : From what i read so far, i guess that could be you ;-)
Hi,
I am having trouble actually using an instance of a controller class in an express router. Where / How do you recommend I instantiate one? In order for me to use the instance methods as route handlers.
great article btw,
Norberto
Your blog posts are great. I like that you keep it short but also informative! Thanks for the motivation
I still don't fully understand how this is problematic:
In tests I could simply inject a mock UserRepo via the constructor, with the same interface as UserRepo. Typescript would be satisfied and I am not actually using the real repo object. Is it because UserRepo is loaded during import?
Awesome content! Alerady bought your book! And I'm really ansious to read the full content of it.
Awesome content! Alerady bought your book! And I'm really ansious to read the full content of it.
Thank you for this well put together and in depth explanation of these two very important concepts!\
Keep up the great work, looking forward to seeing your style of DI in the solidbook.io
Great content,
today I finally understand why and what is dependency injection or should I call this post @inject? xD
Thank you very much.
This is an amazing article, so easy to understand with such great examples.
are you using some library for that because i am not able to access my class function by using interface. i am new to node js and want to build good architecture. please help
Wow super amazing and explained article! Thanks for your work.
Greetings from Portugal.