Dependency Injection in NodeJS: What, Which and How?
"High-level classes should not call the low-level classes directly in order to avoid concretion. Instead, both should depend upon abstractions." Then, in turn, abstractions should not depend upon the details or implementation. Instead, the implementation should be dependent on the abstraction.
The What:
Dependency Injection, (aka DI), is a pattern based on Dependency Inversion Principle (aka DIP) which is one of the 5 principles in SOLID principles (the D in SOLID, duh! 😑). According to this principle, in order to achieve loose coupling between classes that are dependent on each other, the classes that are higher in level of implementation should not call the lower-level class directly which is also called as concretion of classes.
So, if we are not calling the class method directly, how to use that lower-class method in higher-level class? Well, we need an abstraction layer on which both classes should depend on, ultimately removing the dependency.
There are 2 major rules that caters to DIP:
- High-level classes should not call the low-level classes directly in order to avoid concretion. Instead, both should depend upon abstractions.
- Then, in turn, abstractions should not depend upon the details or implementation. Instead, the implementation should be dependent on the abstraction.
Now, in case of TypeScript, this abstraction layer or simply abstraction is made up of interfaces that defines how classes are to be implemented.
As evident from the above diagram, now our main client class need only the interface and we can use whatever service class as we want.
The Which:
Now-a-days, dependency injection is achieved by containers since it provides a more modular and testable approach to handling things.
There are 3 most popular dependency injection container implementations:
- TypeDi
- InversifyJs
- TSyringe
The How:
There are two ways that are particularly followed while using injection:
- Class-based Injections
- Token-based Injections
Here, we will be looking into the class-based injection implementations of above-mentioned containers since you are likely to use this more in order to write your modular, testable and extensible code. The code below is provided like a REST API that you can check out at my GitHub. The example used is a manifestation of below diagram:
TypeDi:
TypeDi was released almost 7 years ago. Its main features include:
- Property based injection
- Constructor based injection
- Singleton and Transient services
- Support for multiple DI containers
It leverages the decorators and decorators' metadata for its operation.
A basic usage example is as follows:
ChairService is a low-level class that implements different methods as below. In order to tell TypeDi about dependency, we use @Service()
decorator.
Now, in order to use this dependency, we inject it using @Inject()
into a higher-level class constructor (or as a property if you prefer) as follows:
Now, TypeDi knows that ChairService is a dependency of RobotService and we can use Container.get()
method to resolve and get our required dependency at our main class as:
So, this is a basic implementation of TypeDi. Either use class or token-based injections and you are practically done.
As you might have noticed that there are no interfaces to link the higher and lower-level classes but achieve the same functionality of loose coupling the classes as described above. This is handled by TypeDi itself, although it might not be good idea to do depending upon type-safety.
Pros:
- Easy to implement
Cons:
- Less features (like switching out the dependency while testing)
- Inadequate Documentation
- Less type-safety since types are being inferred instead of explicit declarations.
InversifyJs:
It is a lightweight container using interfaces created through tokenization. Like TypeDi, it uses decorators and decorators metadata for injections. But we still need to bind the implementations to our interfaces manually.
Singleton and Transient services are here too. And we can also use multiple dependency containers as needed.
A basic usage example is as follows:
First, we need to declare interface of our dependency class:
The next step is same as TypeDi, declaring @injectable()
at the beginning of our classes.
Same as before, we define higher-level class with @inject()
as:
Now, you might have noticed that we are using TYPES
in our inject. This is because InversifyJs recommends using Symbols
for typed definitions which in our case is our interface.
Now, the main hurdle comes where we explicitly bind our interfaces to our implementations. We do this in a separate file named inversify.config.ts
:
Then, we just need to resolve and get the required dependency from the exported container above as:
So, this wraps up the basic dependency injection with InversifyJs. There are also many features related to Container API.
Pros:
- Comprehensive Documentation
- Many and great features
Cons:
- Too much boiler plate code to achieve a simple thing
- Forces use of Symbols
- The code is too verbose
TSyringe:
TSyringe was developed by developers at Microsoft almost 4 years ago. Like InversifyJs, it supports all Dependency Injection Containers features. On top of that, it also supports the resolution of Circular Dependencies. Let's have a look into how it is implemented:
The first 2 steps are same as InversifyJs i.e., declare @injectable()
at top of class and then in higher-level class constructor, define @inject()
but this time, we need not to define Symbol
and can use the interface name as a string i.e., @inject("IChairService")
.
Now, we skip the binding part as was in InversifyJs but just tell our main class to use ChairService as dependency wherever we have injected "IChairService"
in constructor.
This provides a more elegant way of switching out dependencies at the time of development, since we only need to change useClass
at our main module/class.
Pros:
- Simple to write with all the functionality
- No boilerplate codes
- Lightweight
Cons:
- For some people (not us), other than being developed at Microsoft, we didn’t see as such any issue with this.
As per a BundlePhobia, here are some stats of above three:
Container | Bundle Size- Minified (kB) | Download Time (ms) |
---|---|---|
TypeDi | 9.5 | 3 |
Inversify | 49.5 | 13 |
TSyringe | 15.6 | 5 |
Conclusion:
While choosing one of above three, developers have to cater for development experience, size and performance impact, as well as the time it will take to download and build a package since for the CI/CD, it will matter a lot. One of DIP’s main advantage is to stub out the dependencies for our mocked dependency as per requirement during unit testing which is part of test driven development. We can clearly see that TSyringe takes the lead in case of modularity and speed of development while achieving same and more goals.
You can check out all of above implementation at my GitHub.
If you have any suggestions, do comment them so that I may be able to improve this article. Happy Coding. 🧑🏼💻