Implementing Caching in NodeJS Applications with Redis

Redis, which stands for Remote Dictionary Server, is an in-memory database that stores data in key-value pairs. Since Redis is an in-memory database, data is stored in your device’s RAM rather than on a traditional disk which makes the data incredibly fast but also very volatile. Due to this architectural feature, Redis is not used for persisting data like typical databases like MySQL and MongoDB. Instead, it is used to implement caching.

Caching is the process of storing copies of a file in a cache. A cache is a high-speed data storage layer that stores a subset of data. As a result, future requests for that data are served up faster than they would be if the data was served up by accessing its primary storage location. Caching increases application performance by storing regularly used data in caches, thus reducing the time it takes to access them.

This article will teach you how to install Redis on your system and improve your NodeJS application’s performance by implementing caching using Redis.

Installing Redis

Windows Subsystem for Linux (WSL)

Officially, Redis is not supported on Windows. However, you can install it after using the Windows Subsystem for Linux (WSL).

If you don’t have WSL installed on your system, you can follow Microsoft’s guide to installing WSL.

Once WSL is installed, run the following commands in sequence to install Redis:

sudo apt-add-repository ppa:redislabs/redis
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install redis-server

Then, run the following command to start the Redis server:

sudo service redis-server start

macOS

There are multiple ways to install Redis on macOS, but installing Redis with Homebrew is the easiest.

If you don’t have Homebrew installed, follow Homebrew’s installation guide to install it on your system.

With Homebrew installed, run the following command to install Redis:

brew install redis

Next, run the following command to start your Redis server:

brew services start redis 

Linux

Run the following command to install Redis using apt:

sudo apt-get install redis-server

After the installation, Redis will start up automatically. You can check its status by running this command:

systemctl status redis

Alternatively, you can install Redis on Linux using snap-daemon (snapd). If you don’t have snap daemon (snapd) installed on your system, follow snapd’s installation guide.

Docker

On any supported OS, you can also use Docker

Setting up your NodeJS application

To set up your application, you first need to create a new project directory by running:

mkdir redis-tutorial
cd redis-tutorial

Then, initialize npm in your project directory with this command:

npm init -y

The -y flag initializes npm with all its defaults. Next, open your package.json file and add the following code just above the “scripts” section:

"type": "module",

The code-block above changes your scripts to modules. This setting allows you to use the import/export statements and the await keyword outside an async function known as top-level await.

Installing dependencies

Implementing caching in a NodeJS application requires a few dependencies:

  • ExpressJS, a NodeJS framework that provides a robust set of features for web and mobile applications. This framework will make it easier to create your NodeJS server.
  • Axios, a promised-based HTTP client for JavaScript. You’ll need this library to make HTTP requests from your NodeJS server.
  • Node-Redis, a modern, high-performance Redis client for NodeJS. You’ll need this client to interact with Redis from your NodeJS application.

Install the required dependencies by running this command:

npm install express axios redis

Setting up an express app

Create an index.js file and add the following code to create a basic express server:

//index.js
import express from 'express';

const app = express();
const port = process.env.PORT || 3000;

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

The code block above creates an express server and starts listening for traffic on port 3000.

This tutorial will feature the use of a mock API from jsonplaceholder, which is where the data to be cached will be gotten.Add the following code to your index.js file to import Axios and create an asynchronous function that will return data fetched by Axios:

//index.js
import axios from 'axios';

const fetchApiData = async function (id) {
  try {
    const { data } = await axios.get(
      `https://jsonplaceholder.typicode.com/posts/${id || ''}`,
    );

    return data;
  } catch (error) {
    console.log(error);
  }
};

The fetchApiData function takes an optional id parameter (for API requests that return a single resource). Then, it destructures the data property from the “awaited” value of the Axios’ GET request to jsonplaceholder and returns it, wrapped in a try-catch block for error handling.

Now, add the following code to your index.js file to implement the route handlers you’ll need for this tutorial:

//All resource
app.get('/posts', async (req, res) => {
  const data = await fetchApiData();

    res.json(data);
});

//Single resource
app.get('/posts/:id', async (req, res) => {
  const { id } = req.params;
  const data = await fetchApiData(id);

    res.json(data);
});

The first route handler will handle requests to http://localhost:3000/posts and returns all the available resources as JSON, while the second handler handles requests with a wildcard parameter (id) and returns the resource with a matching id in JSON format.

Connecting to Redis

Your Redis server must be running on your system before your application can connect to it. Recall that the commands to start Redis on your operating system were specified in the installation section above.

Next, run this command on your terminal:

redis-server

This command should display the following output:

implementing caching in NodeJS Applications with Redis

Next, import createClient from redis and add the following code to your index.js file:

//index.js
import { createClient } from 'redis';

//Creating a Redis client
const redisClient = createClient();

//Listening for error event
redisClient.on('error', (error) => console.log(error));

//Connecting application to NodeJS
await redisClient.connect();

The code block above creates an instance of a Redis client by calling the createClient function you imported earlier. The createClient method connects to host 127.0.0.1 and port 6379 by default. 
If you want your client to connect to a different host and port, you can pass the details as an argument to the createClient method, like so:
const redisClient = createClient({
    socket: {
        host: '',
        port: 
    },
    //If required
    password: ''
});

Next, the code block listens for an error in the client’s connection using the NodeJS on method and logs the error to the console if something goes wrong with the connection. Finally, it awaits the Redis connection to your server.

Implementing caching

In this section, you’ll implement caching and compare the different response times.

First, use curl to get the approximate response time it takes to make a GET request to http://localhost:3000/posts and http://localhost:3000/posts/1 without caching by running the commands below on your terminal:

curl -o /dev/null -s -w 'Total: %{time_total}s\n'  http://localhost:3000/posts

curl -o /dev/null -s -w 'Total: %{time_total}s\n'  http://localhost:3000/posts/1

Running the commands above returns the time it took to get a response on your terminal, as shown in the image below:

checking response time

As you can see, the responses took approximately 0.7 and 0.5 seconds, respectively (response time will vary depending on internet connectivity and system hardware). Note the response time on your system. You’ll need it to compare the difference when you implement caching.

Next, you will implement caching in both routes by saving the API data to Redis and retrieving it directly from your device’s RAM.

In your index.js file, update your /posts route handler to:

app.get('/posts', async (req, res) => {
  // Getting data from Redis
  const cache = await redisClient.get('posts');

  // If data is stored in Redis
  if (cache != null) {
    console.log('Data from cache');

    res.json(JSON.parse(cache));
  } else {
    // If data is not stored in Redis

    // Get data from API
    const data = await fetchApiData();
    console.log('Data from API');

    // Saving unique data to Redis with expiration
    redisClient.set('posts', JSON.stringify(data), { EX: 600, NX: true });

    res.json(data);
  }
});

In the code block above, you try to fetch the API data from Redis by calling the get and method on your client instance.

The Redis get method retrieves a string value of the key provided or nil if the provided key is not stored in Redis. It takes the key of the value you want to access as an argument — in this case, “posts.”

Next, you check if the data is stored in Redis. If it is, you parse it because get only fetches string values. Then, send the data as a JSON response.

You can learn more about Redis data types in the official Redis documentation.

If the data hasn’t been stored in Redis, send a request to the API by calling the fetchApiData function. Then, store the result in Redis by calling the set method on your client instance.

The Redis set method sets a key to hold a string value in Redis. If the key already holds a value, it’s overwritten by the new value, regardless of its initial type. This behavior might lead to unforeseen issues.

The set method takes three arguments: the key, data to be stored by Redis (string), and an options object.

Then, you pass “posts” as your key, stringify the JSON data (because set only stores strings), and pass the options object with EX set to 600 and NX set to true.

The EX property sets the expiry for the data stored in Redis in seconds. Setting an expiry is essential because data from an API is dynamic and might change at any time. Without an expiry, users may be served outdated data.

The NX property set to true tells Redis only to set the key if it doesn’t already exist (disabling the ability to overwrite data).

You can learn more about the set method options in the official Redis documentation

Finally, you send the data as a response in JSON format. In your index.js file, update your /post/:id route handler to:

app.get('/posts/:id', async (req, res) => {
  // Extracting ID from params object
  const { id } = req.params;

  // Fetching cache from Redis
  const cache = await redisClient.get('post');

  // If data stored in Redis
  if (cache != null) {
    console.log('Data from cache');

    res.json(JSON.parse(cache));
  } else {
    // If data is not stored in Redis

    // Get data from API
    const data = await fetchApiData(id);
    console.log('Data from API');

    // Saving unique data to Redis with expiration
    redisClient.set('post', JSON.stringify(data), { EX: 600, NX: true });

    res.json(data);
  }
});
 

The process in the code block above is the same as in the /posts route handler. The only difference is the size of the data Redis stores. Your finished index.js file should be similar to this:

import express from "express";
import axios from "axios";
import { createClient } from "redis";

const app = express();
const port = process.env.PORT || 3000;

//Creating a Redis client
const redisClient = createClient();

//Listening for error event
redisClient.on("error", (error) => console.log(error));

//Connecting application to NodeJS
await redisClient.connect();

const fetchApiData = async function (id) {
  try {
    const { data } = await axios.get(
      `https://jsonplaceholder.typicode.com/posts/${id || ""}`
    );

    return data;
  } catch (error) {
    console.log(error);
  }
};

//All resources
app.get("/posts", async (req, res) => {
  // Getting data from Redis
  const cache = await redisClient.get("posts");

  // If data is stored in Redis
  if (cache != null) {
    console.log("Data from cache");

    res.json(JSON.parse(cache));
  } else {
    // If data is not stored in Redis

    // Get data from API
    const data = await fetchApiData();
    console.log("Data from API");

    // Saving unique data to Redis with expiration
    redisClient.set("posts", JSON.stringify(data), { EX: 600, NX: true });

    res.json(data);
  }
});

//Single resource
app.get("/posts/:id", async (req, res) => {
  // Extracting ID from params object
  const { id } = req.params;

  // Fetching cache from Redis
  const cache = await redisClient.get("post");

  // If data stored in Redis
  if (cache != null) {
    console.log("Data from cache");

    res.json(JSON.parse(cache));
  } else {
    // If data is not stored in Redis

    // Get data from API
    const data = await fetchApiData(id);
    console.log("Data from API");

    // Saving unique data to Redis with expiration
    redisClient.set("post", JSON.stringify(data), { EX: 600, NX: true });

    res.json(data);
  }
});

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

Now, try to get the response times for GET requests to http://localhost:3000/posts and http://localhost:3000/posts/1 using the same curl commands above. At first, nothing would change regarding the response time because the data hasn’t been cached. But when you make the request a second time, you should see that the response time has significantly reduced:

Checking response time during implementing caching

As seen in the image above, the responses took approximately 0.001 seconds each as opposed to the initial 0.7 and 0.5 seconds. The application’s performance has increased significantly thanks to caching.

For more, you can find the finished project on GitHub.

Conclusion

By now, you’ve learned how to implement caching in NodeJS applications to increase its performance while also decreasing network costs for the client, ensuring data is available during network outages, and improving responsiveness in your application.

If you liked this content, here are some additional tutorials you may want to check out:

Read more about:

node.js redis

David is a software developer and technical writer with experience building scalable backend infrastructure for web applications and writing various in-depth articles related to backend-focused software engineering for companies such as Digital Ocean, Fixate.io, and Draft.dev, to mention a few. He is primarily interested in exploring and contributing to emerging technology.