Simple in-memory cache in Node.js

In the past I had explained how to use redis as an external source to store and access cached data. If you are interested, read this article.

However, not all solutions require the use of an external source. In case you have no idea how big the application will reach in the early days, using an internal cache can save you a lot of deployment time.

But you have to pay attention to one thing, if your application grows fast or if you already have a good number of daily requests, I always recommend using an external source. This is because by storing data in your application's cache, you will increase your application's memory leaks.

I know a lot of people who don't care about memory leaks, however, if your application consumes a lot of RAM, the system may interrupt the application's execution.

But of course, it's always good to monitor the amount of RAM being used on the server or do some load tests in the development environment, and then take the best solution for the production environment.

Let's code

The idea of this Api is to make an http request to an external Api, from which we will get a whole according to the id parameter. And since we're probably going to make more than one request in a given amount of time, we're going to cache that whole.

That is, when we make the http request for the first time we will store the data in the cache, but the remaining requests will be returned from the cache. However the data will be persisted in the cache for only fifteen seconds.

Now let's install the following dependencies:

npm install express node-cache axios

Now let's create a simple API:

const express = require("express");

const app = express();

app.get("/", (req, res) => {
  return res.json({ message: "Hello world 🇵🇹" });
});

const start = (port) => {
  try {
    app.listen(port);
  } catch (err) {
    console.error(err);
    process.exit();
  }
};
start(3333);

Let's now create the route to fetch a whole to the external API:

app.get("/todos/:id", async (req, res) => {
  try {
    // Logic goes here
  } catch () {
    // Some logic goes here
  }
});

So first we have to get the id parameter to get its to-do. Then we will make the http request using axios. Finally, let's return the data from the response.

const axios = require("axios");

// Hidden for simplicity

app.get("/todos/:id", async (req, res) => {
  try {
    const { id } = req.params;
    const { data } = await axios.get(`https://jsonplaceholder.typicode.com/todos/${id}`);
    return res.status(200).json(data);
  } catch () {
    // Some logic goes here
  }
});

Now we just need to take care of the http request in case an error occurs. In this case, let's go to the response object and get the status and return it with the .sendStatus() method.

app.get("/todos/:id", async (req, res) => {
  try {
    const { id } = req.params;
    const { data } = await axios.get(`https://jsonplaceholder.typicode.com/todos/${id}`);
    return res.status(200).json(data);
  } catch ({ response }) {
    return res.sendStatus(response.status);
  }
});

As you may have already tested, whenever you make an http request, we are constantly going to the external API to get the data.

So the response time is always high. However now we are going to start working on our middleware to check the cache first before going to the controller.

But first we have to import the node-cache into our project and create an instance of it. Like this:

const express = require("express");
const NodeCache = require("node-cache");
const axios = require("axios");

const app = express();
const cache = new NodeCache({ stdTTL: 15 });

// Hidden for simplicity

As you may have noticed in the code above, it makes it explicit that each property that remains in the cache will have a lifetime of fifteen seconds.

Now we can start working on our middleware:

const verifyCache = (req, res, next) => {
  try {
    // Logic goes here
  } catch () {
    // Some logic goes here
  }
};

First we have to get the id from the parameters, then we will check if there is any property with the same id in the cache. If there is, we will get its value, however, if it does not exist, it will proceed to the controller. If an error occurs, it will be returned.

const verifyCache = (req, res, next) => {
  try {
    const { id } = req.params;
    if (cache.has(id)) {
      return res.status(200).json(cache.get(id));
    }
    return next();
  } catch (err) {
    throw new Error(err);
  }
};

Now we have to go back to our endpoint where we're going to get the to-do and we're going to add our middleware. Just like we will add the data to the cache as soon as we get it from the http request.

app.get("/todos/:id", verifyCache, async (req, res) => {
  try {
    const { id } = req.params;
    const { data } = await axios.get(`https://jsonplaceholder.typicode.com/todos/${id}`);
    cache.set(id, data); // also added this line
    return res.status(200).json(data);
  } catch ({ response }) {
    return res.sendStatus(response.status);
  }
});

The final code should look like this:

const express = require("express");
const NodeCache = require("node-cache");
const axios = require("axios");

const app = express();
const cache = new NodeCache({ stdTTL: 15 });

const verifyCache = (req, res, next) => {
  try {
    const { id } = req.params;
    if (cache.has(id)) {
      return res.status(200).json(cache.get(id));
    }
    return next();
  } catch (err) {
    throw new Error(err);
  }
};

app.get("/", (req, res) => {
  return res.json({ message: "Hello world 🇵🇹" });
});

app.get("/todos/:id", verifyCache, async (req, res) => {
  try {
    const { id } = req.params;
    const { data } = await axios.get(`https://jsonplaceholder.typicode.com/todos/${id}`);
    cache.set(id, data);
    return res.status(200).json(data);
  } catch ({ response }) {
    return res.sendStatus(response.status);
  }
});

const start = (port) => {
  try {
    app.listen(port);
  } catch (err) {
    console.error(err);
    process.exit();
  }
};
start(3333);

Some manual tests that have been done using insomnia to see the difference in response times:

Usually when I make a request to the external Api, it takes an average of 350ms. Once cached, it takes an average of 1.6ms. As you can see, we have a big performance gain just by using this strategy.

What about you?

What caching solution do you use?

19