How to use dependency injection in NodeJS TypeScript projects with Inversify
Dependency injection (DI abbreviated) is a technique that removes internal dependencies from the implementation by enabling these dependencies to be injected externally. All of this is a subset of Inversion of Control Principle (IoC abbreviated). This popular principle is based on an idea that objects should not create objects on which they depend to perform some activity. In other words high level modules in an application shouldn't depend on the low level modules; both should rather depend on abstractions.
Let's imagine you have an application that retrieves some data from a MongoDB instance, you shouldn't have some code like this:
Although it's a totally working code, I can't even count the total amount of anti-pattern and architectural errors in this snippet. The part I would like to show you to demonstrate the pattern is listCars method.
The listCars method is directly coupled to database implementation, in our case, mongodb client. What would happen if for any reason the team needed to change from mongodb to other non-relational database, or even for a relational database like postgres, moreover for a non-database style like flatfiles in disk? (are there use cases for it? 😅)
The answer: re-work and headache.
"But I can make it work again in 5 minutes of refactoring" - Some Internal Dev
In fact, making those suggested changes appears to be trivial and fast to do, but remember, this is a fictional scenario and there is only 1 method. Imagine a hundred of methods in dozens of classes. Believe me when I say: headache 😵
The real solution: Dependency Injection
Our code meets Inversify
Inversify is powerful and lightweight inversion of control container for JavaScript & NodeJS. It implements IoC and allow us to inject dependencies, uncoupling our method from any implementation details.
Using inversify, we can switch between any implementation with zero impact in the code. No refactoring, no breaking changes.
Above I wrote an database example but it could be anything writable in code. External sources, Databases, Usecases, Controllers, Frameworks etc. Everything can (should) be switched with zero (or minimum) impact in the current code.
The first thing you need to do is to define an interface to be used as an abstraction of the implementation:
Then, you create an concrete implementation of this contract. As can be seen below, there is extra configurations in the class.
The @injectable is a decorator available in the Inversify library, it allows you to indicate the current class can be "injected" using the container.
The next thing to do is to write an unique identifier to this specific injection, it can be made using a plain old Javascript Object.
Don't worry about Symbols, it's just a native way of JS to create unique identifiers. According to MDN Web Docs:
Every Symbol() call is guaranteed to return a unique Symbol. Every Symbol.for("key") call will always return the same Symbol for a given value of "key".
The last step is to glue everything up using the .bind() method from Inversify container:
A simple manner to understand the bind method above is to think straightforward: "Everytime I invoke this Interface, given this specific Locator, I'll get this specific implementation".
Now we are ready to go! Let's use this container and inject a new repository instance in our Car class.
There is an interesting point here. We inject a repository using Car constructor method. Notice that Car class doesn't know anything about repository implementation but only its interface. This allows us to change the implementation with no impact in the Car class.
For example, we can stop using MongoDB and change to FlatFiles instead:
The only part to put your hands on is the container:
With this mininum change we switched from MongoDB to other storage schema without no impact in the main logic (Car class).
Conclusion
Using Inversify to inject dependencies and help us to following the IoC principle is a great way to keep our code less coupled and cleaner. We saw that we can switch from any implementation without impact the main logic which are receiving the specified abstraction.
Thank you for reading until the end. <3
Software Engineer @Centralogic | Beta MLSA 🛡️ | EX- GDSC Lead’ 23 | Nodejs | ASP.Net | Devops | Backend Dev | Leetcode 300+
9moReally Helpful!!
Software Developer | Node.js | Typescript | AWS | Express | MySQL
4yGreat article! Exemplifies very well how to use DI