DEV Community

loading...
Cover image for Dependency Injection In JavaScript

Dependency Injection In JavaScript

paularah profile image Paul Arah ・3 min read

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;
};
Enter fullscreen mode Exit fullscreen mode

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
})()
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (3)

Collapse
jackmellis profile image
Jack

I'm going to plug my own DI library powered by typescript types:
npmjs.com/package/jpex
DI is a hugely underappreciated pattern in the javascript ecosystem that is used widely in almost every other language/framework!

Collapse
paularah profile image
Paul Arah Author

Thanks Jack! your DI library looks straightforward and easy to use, I'd definitely give it a spin.

Collapse
arvindpdmn profile image
Arvind Padmanabhan

The example code above is useful. For beginners who wish to understand DI at a concept level, you can read this article: devopedia.org/dependency-injection

Forem Open with the Forem app