Dependency Injection In JavaScript
Paul Arah
Posted on March 26, 2021
Writing code that is resilient in the face of changing requirements needs an intentional application of techniques that achieve this goal. In this article, we'll explore dependency injection as one of those techniques.
Take a look at the code snippet below.
const getData = async (url) => {
const response = await fetch(url);
const data = await response.json();
return data;
};
This function retrieves a resource across the network using the Fetch API and returns it. While this works, from a clean and maintainable code perspective, there are quite a number of things that could go wrong here.
- If our requirements change in the future and we decide to replace the Fetch API with say another HTTP client like Axios, we would have to modify the whole function to work with Axios.
- The Fetch API is a global object in the browser and isn't available or might not work exactly as intended in an environment like Node.js where we would be running our test.
- When testing we might not want to actually retrieve the resource from across the network, but there's currently no way to do that.
This is where dependency injection comes into play. At the core of it, dependency injection is giving the dependency(s) our code needs from the outside rather than allow our code to directly construct and resolve the dependencies as we have done in the example above. We pass in the dependencies our code needs as a parameter to the getData function.
const getData = async (fetch, url) => {
const response = await fetch(url);
const data = await response.json();
return data;
};
(async => {
const resourceData = await getData(window.fetch, "https://myresourcepath");
//do something with resourceData
})()
The intent behind dependency injection is to achieve separation of concerns. This makes our code more modular, reusable, extensible and testable.
At the core of javascript are objects and prototypes, so we can do dependency injection the functional or object-oriented way. Functional programming features of javascript like higher-order functions and closures allow us implement dependency injection elegantly.
const fetchResource = (httpClient) => (url) =>
httpClient(url)
.then((data) => data.json)
.catch((error) => console.log(error));
The fetchResource function takes an instance of our HTTP client and returns a function that accepts the URL parameter and makes the actual request for the resource.
import axios from "axios";
const httpClient = axios.create({
baseURL: "https://mybasepath",
method: "POST",
headers: { "Access-Control-Allow-Origin": "*"}
});
const getData = fetchResource(httpClient);
getData("/resourcepath").then((response) => console.log(response.data));
We replaced the native fetch with Axios, and everything still works without meddling with the internal implementation. In this case, our code doesn't directly depend on any specific library or implementation. As we can easily substitute for another library.
The object(function in this case) receiving the dependency is often referred to as the client, and the object being injected is referred to as the service.
A service might require different configurations across the codebase. Since our client doesn't care about the internal implementation or configuration of a service, we can preconfigure a service differently as we've done above.
Dependency injection enables us to isolate our code(business logic) from changes in external components like libraries, frameworks, databases, ORMs, etc. With proper separation of concerns, testing becomes easy and straightforward. We can stub out the dependencies and test our code for multiple ideal and edge cases independent of external components.
In more complex use cases, usually bigger projects, doing dependency injection by hand is simply not scalable and introduces a whole new level of complexity. We can leverage the power of dependency injection containers to address this. Loosely speaking, dependency injection containers contain the dependencies and the logic to create these dependencies. You ask the container for a new instance of a service, it resolves the dependencies, constructs the object and returns it back.
There are a number of Javascript dependency injection container libraries out there. Some of my personal favourites are TypeDI and InversifyJS. Here is an example demonstrating basic usage of Typedi with JavaScript.
import { Container } from "typedi";
class ExampleClass {
print() {
console.log("I am alive!");
}
}
/** Request an instance of ExampleClass from TypeDI. */
const classInstance = Container.get(ExampleClass);
/** We received an instance of ExampleClass and ready to work with it. */
classInstance.print();
The technique of dependency injection cuts across different programming languages. As a general rule of thumb, dependency injection can be done with languages that allow the passing of functions and objects as parameters. Some popular HTTP frameworks like NestJs and FastAPI come with an in-built dependency injection system.
Posted on March 26, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.