Dependency Injection in JavaScript, a Node.js example

As developers, we are tasked with reducing complexity. Complexity is all around us, and good code organization reduces complexity while at the same time supporting increased flexibility, ease of change, quicker onboarding, faster debugging, and my favorite, better testing.

Reduced Complexity, Better Testing

In this article, I discuss one of the widespread strategies for code organization: Dependency Injection (DI). In doing so, we examine how DI reduces complexity, and makes testing easier in a JavaScript application. For an article on DI strategies for Ruby applications, also see this blog post.

What is Dependency Injection?

Imagine some module A that depends on another module B to do its work. How does A gain access to B?

One option is to import or require the dependency:

There is nothing illegal about this approach, but one downside is that A and B are now tightly coupled. A is bound to B, and if we ever wanted to use something other than B down the road, we may find ourselves touching a lot of code.

With dependency injection, however, instead of using import or require to access B at build time, A exposes a means for B to be injected at runtime, and the dependency injection mechanism takes over from there by providing A what it needs to do its work.

Example: No Dependency Injection

Suppose we are tasked with developing an asynchronous method that issues an HTTP request and returns a Promise to let callers track the status of this operation.

Here’s an initial implementation using Node.js:

The getter uses the https module to do its work, and that dependency is loaded in the beginning of the file, a common approach. Unfortunately, this means that in order to test this function, we must mock https somehow, ensuring that mocking occurs before the ResourceGetter script is ever loaded into memory.

Such an approach may look like this, where https is mocked…

Here, we are using proxyquire to stub https. While this works, it leaves something to be desired as we’ve just added another package and written code that is unnecessary. After all, proxyquire cannot be a hard requirement for Node.js unit testing. What can be done about this?

Example: Dependency Injection

This is where dependency injection can be useful. With DI, our implementation changes slightly:

With this update, testing no longer requires proxyquire, as we simply inject the mocked https:

While the changes needed to implement dependency injection were slight, the effect is profound. By decoupling code, we not only simplify testing but also the application in question. Furthermore, substituting one dependency for another has just become trivial, should the need arise, making our application more extensible and future-proof.

Ultimately, dependency injection yields well-defined interfaces for connecting application components to reduce complexity, which is a win in almost all cases. So, the next time you’re developing a component, module, service, or the like, utilize DI and enjoy the fruits of your labor when you revisit the code months later.

Leave a Reply