28 May 2019
What is a dependency injection?
DI is a pattern where, instead of creating or requiring dependencies directly inside a module, we pass them as parameters or reference.
At first glance, it might be quite difficult to understand. But it’s easier than you think.
Let’s imagine a simple service module:
It looks fine. The service is responsible for business logic and user repository is responsible for communication with the data source. But there are two problems here.
First of all, the service is coupled with a specific repository. If we wanted to change it to something else, then we would have to modify the entire code. I’m sure you know one of the common programming rules – program to an interface, not an implementation. This is where we violate that rule!
A second problem is the testability of this module. If we wanted to find out if
getUsers method works, we would need to stub
usersRepository using Sinon, Jest.mock or any other stubbing library.
It looks complicated, doesn’t it? Let’s use dependency injection to fix it up!
What we need to do is to pass
usersRepository as a parameter instead of requiring it directly.
As you can see, the service is no longer paired with a repository module but requires
usersRepository to be passed to it. It has a major impact on testability:
We could drop sinon from our dependencies and replace it with the injection of the users repository. This approach allows us to unit-test much more complex cases, without the need for stubbing.
What’s more, by decoupling service and repository, you gain the freedom to change implementation at any point. Cool!
Dependency injection in Node.js – classes vs functions
Another reason why dependency injections are not popular in the Node.js ecosystem might be a myth that DI is an OOP-only concept. That’s most definitely not true!
Of course, it’s obvious how to inject dependencies in classes. We have a constructor, where we can inject dependencies one by one or by a single object. At TSH, we’re big fans of reducing the number of params being passed to class/function by enclosing them in the object.
You can easily use destructuring to access specific dependencies you need. It’s even better in TypeScript where you can specify required dependencies by type.
We create a function that takes dependencies as parameters and then returns another function/object with a specific implementation. Because of the closure we created, we have access to dependencies from the inner functions.
As you can see, DI is not only about classes!
💡 See also
Need top-class Node.js development?
🛠 Rely on Poland’s biggest Node team to build or expand your app with success. Technology professionals rate our software development delivery 4.9 on Clutch.
Orchestration and tooling
The visible downside of DI is the requirement to set everything up beforehand. Following the previous example, if I want to create users service, I need to make a repository, mailer and logger. What’s more, both repository and mailer could have their own dependencies, so I need to create the whole structure.
We need to prepare some things before we are able to create the service. This approach is what we call service container – a special module where all of the public services/factories and so on are orchestrated.
Of course, this one is simple and requires a lot of manual work. Still, there are Node.js DI libraries that can do some of it for us.
There are a few options available. The most popular ones are Awilix, Inversify and TypeDI.
At TSH, we’re mostly using Awilix, but consider TypeDI to be very promising.
By using Awilix, we can create a special container that will resolve dependencies on its own. The only thing we need to provide is base building blocks. In our case, it’s about services like DataSource, Logger or parameters like
The rest (UsersService, UsersRepository, Mailer) will be resolved automatically by Awilix. The only thing we need to provide is a type of resolver (for example, we need to tell that UsersService is a class).
When I call the
resolve method on an Awilix container, it goes through all of the constructor/function parameters and checks if there is a dependency with the same name in our container (Awilix solution is not based on types – even for TS – but on the dependency name) and tries to resolve it.
By doing this, we don’t need to create our dependencies one by one. We only need to provide building blocks and then call the resolve method to get a specific service. I strongly encourage you to check both Awilix and TypeDI (for TypeScript, it allows to get dependencies by types).
Friend or foe?
Dependency injection provides a lot of flexibility not only when it comes to testing, but also when working with an app. It allows us to create modules that are fully independent of each other.
By using some additional tools such as a dependency container, you can make the task of modules building ever easier. At TSH we use DI in every Node.js project and we can’t imagine working without it.