Back

Forever Functional: Dependency Injection and Higher Order Routing in Node.js

Forever Functional: Dependency Injection and Higher Order Routing in Node.js

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
}

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);

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...');
    }
  };

When defining routes we’d write then:

const app = express();
.
.
.
app.get('/catfact', catFactGet);

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...');
    }
  };

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));

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]

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
    .
  };

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));

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

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

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

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay 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!