29 June 2023
JavaScript dependency injection in Node – friend or foe?
Dependency injections, when implemented properly, offer a lot of benefits. They can make your system more flexible and modular, and even help you write testable code. Unfortunately, the implementation is not straightforward and it is especially rarely used when it comes to Node.js. But today I’m going to show you that not only is dependency injection in Node.js a thing, but also that it can be done with functions too! Let’s get to coding!
When you start working with Node.js, one of the first concepts you learn is the module pattern. There is nothing better than reusability and modularity, isn’t it?
But according to some, you don’t need dependency injection in Node.js because there’s require. Personally, I don’t really agree with that. Why?
Let’s start by clearly establishing what JavaScript dependency injection in Node.js actually is.
What you will learn – the dependency injection in Node.js overview
In today’s article, I’m going to show you the potential of dependency injections in Node.js to make your life easier. Specifically, you’re going to get:
- practical explanation of the dependency injection concept (We’re going to write code examples),
- an overview of dependency injection solutions in Node.js for both object oriented programming and functions (yes!),
- a quick introduction into setting up a DI-based project, in particular the orchestration and tooling.
Let’s go for it!
What is a dependency injection in Node.js?
Dependency injection in Node.js or JavaScript in general is a well-known technique, which can make it much easier to produce independent and scalable modules. It can be used in many programming languages and frameworks. However, when it comes to Node.js, dependency injections are not quite as popular as they could be. To some extent, it’s a result of certain misconceptions.
To put it in other words, dependency injection in Node.js 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.
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.
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.
But JavaScript is not only about classes (we all know that it’s just syntax sugar). So how can we achieve the same with functions only? The answer is simple – parameters.
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
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. Let’s make a dependency injection container. We’re also going to create some configuration files.
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.
TypeDI and Awilix are quite similar and work with both JavaScript and TypeScript. On the other hand, Inversify is TypeScript-only.
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 level
or templates
.
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).
Solutions for dependency injection in Node.js – friends or foes?
That’s about it! Let’s sum things up:
- Node dependency injection provides a lot of flexibility not only when it comes to easier unit testing and testing in general, but also when working with an app.
- It allows us to create and implement modules that are fully independent of each other and it is always a welcome thing in modern object oriented programming and software development. Most developers can appreciate the benefits dependency injection pattern provides.
- By using some additional tools, for example a dependency container, you can make the task of modules building ever easier, avoiding quite a few issues brought up in this article.
At TSH we use DI in every Node.js project, in both small and large projects, and we can’t imagine working with our code without it.
I’m going to return to the topic of dependency injection in Node.js as well as well as other design patterns such as dependency inversion and dependency inversion principle in the coming articles so stay tuned!
💡 Do you want more expert Node.js content? Check out these pieces
Are you looking for Node.js developers who can use dependency injection in commercial projects?
Our portfolio is full of practical implementations of similar techniques and concepts. Check it out!