In today's article, we will create a simple API where we will consume an external resource to get some data related to a user. We will implement a cache system to manage the users that have already been acquired and persist with them for a certain period.

Introduction

Redis is a good choice for caching mechanisms because of its low latency, easy relief from relational and non-relational database pressure, and ease of use. It is ideal for handling heavy traffic and being easy to scale your apps.

Prerequisites

Before going further, you need:

  • Node
  • NPM
  • Redis

In addition, it is expected to have a basic knowledge of Express.js to complete the project.

Getting Started


Create Project Setup

As a first step, create a project directory and navigate into it:

mkdir node-redis

cd node-redis

Then we can create the development environment:

npm init -y

And then, we can add the following property to our package.json.

{
  "type": "module",
}

With the basic project setup done, we can move on to the next step.

Install dependencies

The next step is to install the packages needed to build the application. Run the command below in your terminal:

npm install express ioredis axios

By running the above command, you will install the following dependencies:

  • Express - a minimalist web framework
  • ioredis - a robust Redis client for Node.js
  • Axios - a popular HTTP client for Node.js

The fundamental blocks of today's project will be express and ioredis, this is because the example that will be given today with Axios can be easily replaced by a query to the database or other data stores.

Configure the Express API

Before proceeding to configure the API, you need to create the entry file:

touch server.js

The main.js file will contain all the code for today's examples. In that case, in the main.js file, write the following API:

import express from "express";

const app = express();

app.get("/users/:id", async (req, res) => {
  const { id } = req.params;

  return res.json(id);
});

app.listen(3000);

The next step will be to import Axios into our entry file and make an HTTP request to a public API in order to get mocked data.

import express from "express";
import axios from "axios";

const findUserById = async (id) => {
  const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
  return data;
};

// ...

With the function created, we can now implement it in our route.

import express from "express";
import axios from "axios";

const findUserById = async (id) => {
  const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
  return data;
};


const app = express();

app.get("/users/:id", async (req, res) => {
  const { id } = req.params;
    
  const data = await findUserById(id);
    
  return res.json(data);
});

app.listen(3000);

Currently, if we make the HTTP request to our API, we will always end up consuming the external resource. Consequently, we will be loading the external API and the HTTP requests will have a longer response time, especially in this case where we can have several consecutive requests for the data of a specific user. And even in this case, it is data that is not updated very regularly.

Now our next step will be to import ioredis, and then we will create a client with it.

import express from "express";
import Redis from "ioredis";
import axios from "axios";

const findUserById = async (id) => {
  const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
  return data;
};

const app = express();
const redisClient = new Redis();
  
// ...

With our Redis client created, we need to create a proxy in order to ensure that the application continues to work normally even if Redis fails.

import express from "express";
import Redis from "ioredis";
import axios from "axios";

const findUserById = async (id) => {
  const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
  return data;
};

const app = express();
const redisClient = new Redis();

const redis = new Proxy(redisClient, {
  get(target, property, receiver) {
    if (redisClient.status !== "ready") {
      return () => false;
    }
    return Reflect.get(target, property, receiver);
  },
});

// ...

Now with our Redis client fully configured, we can move on to the next step.

Explaining the cache process

The idea is to check if the key (in this case, it's the user's id) already exists in Redis; if it already exists, the value of that same key (which corresponds to the user's data) should be returned. However, if the key does not exist in Redis, we will have to make the HTTP request to the external resource, and only after obtaining the data will we store it in Redis.

This means that all subsequent requests with the same key will always be returned from the cache until the key expires.

Now with this information, we can create the middleware:

import express from "express";
import Redis from "ioredis";
import axios from "axios";

// ...

const app = express();
const redisClient = new Redis();

const redis = new Proxy(redisClient, {
  get(target, property, receiver) {
    if (redisClient.status !== "ready") {
      return () => false;
    }
    return Reflect.get(target, property, receiver);
  },
});
  
const cacheMiddleware = async (req, res, next) => {
  const { id } = req.params;

  try {
    const result = await redis.get(id);

    if (result) {
      return res.json(JSON.parse(result));
    }

    next();
  } catch (error) {
    next(error);
  }
};
  
// ...

Then we can add the middleware to our API route and set the key in the Redis store:

import express from "express";
import Redis from "ioredis";
import axios from "axios";

const findUserById = async (id) => {
  const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
  return data;
};

const app = express();
const redisClient = new Redis();

const redis = new Proxy(redisClient, {
  get(target, property, receiver) {
    if (redisClient.status !== "ready") {
      return () => false;
    }
    return Reflect.get(target, property, receiver);
  },
});

const cacheMiddleware = async (req, res, next) => {
  const { id } = req.params;

  try {
    const result = await redis.get(id);

    if (result) {
      return res.json(JSON.parse(result));
    }

    next();
  } catch (error) {
    next(error);
  }
};

app.get("/users/:id", cacheMiddleware, async (req, res) => {
  const { id } = req.params;

  const data = await findUserById(id);
  await redis.set(id, JSON.stringify(data), "EX", 15);

  return res.json(data);
});

app.listen(3000);

The redis.set() method takes four arguments, as you may have noticed, and if it is your first time, it might look overwhelming, but don't sweat, I will explain each one:

  • The first argument is the key we want to store;
  • The second argument is the data (which cannot be saved as json, so it must be stringified);
  • The third argument is the `secondsToken` which in this case corresponds to the `EX` expiration;
  • The fourth and final argument is the duration in seconds of the expiration token.

How to start the application

You can start the application by running node main.js in the root folder of your project.

I hope you enjoyed this tutorial. Stay tuned for more.