DEV Community

Cover image for Dependency Injection in Node.js - Higher Order Routing With Express
OpenReplay Tech Blog
OpenReplay Tech Blog

Posted on • Originally published at blog.openreplay.com

Dependency Injection in Node.js - Higher Order Routing With Express

by author Federico Kereki

When you get used to applying Functional Programming, you may find that it fits many situations. In this case let's discuss routing in a Node+Express application, but aiming for highly testable code.
In this article, we'll discuss what injection is, its advantages, and an example of injection employing higher-order functions.

Processing routes - bad and good ways

Route handlers, to reply to requests, may need to do things such as:

  • access some (SQL or NoSql) database
  • get data from some cache (like Redis)
  • call some other API

You may directly insert the needed code in the function that handles the route, but that raises problems of its own. For instance, let's just consider the first item, accessing some database. You could have logic together for using the database and for producing the answer, but that has some drawbacks.

  • Responsibilities (doing calculations, plus accessing or persisting data) are mixed so your functions do not follow the "single responsibility" design principle
  • Code gets muddied because business logic and data logic are all together
  • Code duplication will appear: if other route handlers need the same data they will have to duplicate the needed code
  • Testing will become harder because of the added route handler responsibilities. If the handler does more things, you have to write more complex tests

A common solution to these problems involves dependency injection. In this technique, a function receives other functions it depends on (its dependencies) from outside. In other words, a handler wouldn't directly access a database or a cache, or call another API. Instead, it would do it indirectly through externally injected functions. Injection may be done explicitly or implicitly (with an injector) but that's not relevant for this article.

With this idea, a handler that needs to get some data shouldn't know how to access the database. Similarly, if it needs to call a service it won't have to know how to construct the service call. It will, instead, delegate those responsibilities to the injected functions, which will do whatever is needed. The handler won't even know if it's getting data from a database, a cache, or some other API; just that by calling some injected function it will get whatever it needs.

Injection is also great for testing. If you want to unit test a handler that directly calls a service or accesses a database, it's a complex thing. For mocking API calls, tools such as Nock may be used. Mocks for databases are harder to use: going with an actual (test) database works, but tests become slower, and you have plenty of work resetting the database for every test. But, if access is achieved by injection, all you need to do is provide a mock function (easily done in Jest or other testing frameworks) and the tested handler won't know the difference.

So, how can we implement this injection? Actually, in many ways. Let's now see a simple example, that uses higher-order functions.

Learning about cats - the bad way

Implementing an API with Node+Express, we could want an endpoint that would provide a random cat fact. We can get this by courtesy of the free "Cat Fact" API. Each time you call this API you get some trivia in JSON format, such as:

{
  "fact": "In 1987 cats overtook dogs as the number one pet in America.",
  "length": 60
}
Enter fullscreen mode Exit fullscreen mode

The controller for the route could directly call the API, but as we said, that would make unit testing harder. Instead, we'll inject a function that calls the cat API into the route controller. Let's now write such a function, and later we'll see how to use it.

// in some module with all API calls
import axios from 'axios';

export const getCatFactApi = () =>
  axios.get('https://catfact.ninja/fact').then((x) => x.data);
Enter fullscreen mode Exit fullscreen mode

We just use Axios to do a GET of the API endpoint, and we return the data, the trivia shown above. Axios calls return a promise, which is quite proper for our needs. Given this function, we could write the controller as follows.

// import getCatFactApi from its module

export const catFactGet =
  async (req, res, next) => {
    try {
      const newFact = await getCatFactApi();
      res.status(200);
      res.send(newFact.fact);
    } catch (e) {
      res.status(500);
      res.send('Who knows what happened here...');
    }
  };
Enter fullscreen mode Exit fullscreen mode

When defining routes we'd write then:

const app = express();
.
.
.
app.get('/catfact', catFactGet);
Enter fullscreen mode Exit fullscreen mode

This works -- but we haven't injected anything, and the catFactGet is hardcoded to directly call the API! Let's fix this and actually use injection as we promised.

Learning about cats - the good way

We need to provide the API-calling function as a parameter to our route handler. However, Express will call our handler with req, res, and next parameters, not directly allowing us to provide anything extra. But, we can write a higher-order function that will provide the needed dependency. We would write the following.

export const catFactGet =
  (someCatApi) => async (req, res, next) => {
    try {
      const newFact = await someCatApi();
      res.status(200);
      res.send(newFact.fact);
    } catch (e) {
      res.status(500);
      res.send('Who knows what happened here...');
    }
  };
Enter fullscreen mode Exit fullscreen mode

Check this code out carefully! It's a unary function that receives a single parameter (someCatApi) and returns a ternary function that expects req, res, and next, as Express requires. The returned function uses the provided someCatApi function to access the API; nothing's hardcoded here. Now, what remains is to pass the proper API-accessing function to the route.

const app = express();
.
.
.
app.get('/catfact', catFactGet(getCatFactApi));
Enter fullscreen mode Exit fullscreen mode

Note the big difference! We're now saying that the handler for the /catfact route will be whatever catFactGet() returns -- the proper handler, calling the real API! If we had wanted to test the handler instead, we'd have done something like the following.

const fakeCatApiCall = ...create a mock somehow;   // [1]
const handlerToTest = catFactGet(fakeCatApiCall);  // [2]
Enter fullscreen mode Exit fullscreen mode

In [1] we'd define an appropriate mock function with known behavior. Then we'd create a specific version of the route handler at [2]. The created handler will use our fake cat API call when it needs to get a new fact. After having done that, all that's left to do is properly testing handlerToTest(), by providing req and res mock objects (a library such as mock-req-res comes in handy) and checking what the handler does.

Some other ways

Is this the only way we could have handled the injection? A higher-order function does the job, as we've seen. There are at least two or three more solutions; let's discuss them for thoroughness. We may use arrow functions or the bind() method, as we'll see. We would start by writing a function with four parameters.

export const catFactGet =
  async (someCatApi, req, res, next) => {
    .
    . // as earlier
    .
  };
Enter fullscreen mode Exit fullscreen mode

Then we could use it as a handler employing an arrow function as follows.

const app = express();
.
.
.
app.get('/catfact', (req, res, next) => catFactGet(getCatFactApi, req, res, next));
Enter fullscreen mode Exit fullscreen mode

We could also use bind() to fix the first parameter.

const app = express();
.
.
.
app.get('/catfact', catFactGet.bind(null, getCatFactApi));
Enter fullscreen mode Exit fullscreen mode

In both cases, the assigned handler is a function with the API-calling function "burned in". There are yet more ways of handling this with functional programming techniques, currying and partial application, but we'll leave those for a separate article!

Open Source Session Replay

Debugging a web application in production may be challenging and time-consuming. OpenReplay is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue.
It’s like having your browser’s inspector open while looking over your user’s shoulder.
OpenReplay is the only open-source alternative currently available.

OpenReplay

Happy debugging, for modern frontend teams - Start monitoring your web app for free.

Summary

We have discussed how to use higher-order functions to inject dependencies into route handlers. This technique makes for clearer code, with separation of concerns and simplified testing. For impartiality, we also discussed another couple of (less functional) ways to achieve the same result. Using any of the techniques shown in this article will enhance your Node+Express code, so give them a chance!

Top comments (0)