How to use Redis for Caching API Responses in Node.js

·3 min read
How to use Redis for Caching API Responses in Node.js

Caching involves storing frequently accessed data in a fast-access storage medium to reduce the load on the main database and improve response times. In this blog post, we'll explore how to set up and utilize Redis as a caching solution in a Node.js application, using a practical example.

The Scenario

Imagine you're developing an API that fetches user information from an database or external service, such as the JSONPlaceholder API, which provides dummy data for testing. To enhance performance, you want to cache the responses from this API using Redis, a popular in-memory data store. This will help reduce the number of requests made to the external service and improve the overall response time of your API.

Setting Up Redis

Before we delve into the code, let's briefly cover how to set up Redis in your Node.js application. In the code snippet you provided, the Redis client is established using the "ioredis" library:

import Redis from "ioredis";
 
const redisClient = new Redis({
  port: 6379,
  host: "127.0.0.1",
});
//If node process ends, quit redis connection
process.on("SIGINT", async () => {
  await redisClient.quit();
});
 
export default redisClient;
 

Here, the Redis client is configured to connect to the local Redis server running on the default port (6379) and host (127.0.0.1). Additionally, a signal handler is set up to ensure that the Redis connection is properly closed when the Node process is terminated.

Implementing Caching Logic

The heart of caching lies in the logic that determines when to fetch data from the cache and when to retrieve it from the external source. Let's break down the relevant parts of the provided code:

const USERS_API = "https://jsonplaceholder.typicode.com/users/";
 
app.get("/users", async (req: Request, res: Response) => {
  try {
    let users = await UserCache.fetchAll();
    if (!users) {
      console.info("Users Cache miss");
      const response = await axios.get(`${USERS_API}`);
      if (response && response.data) {
        users = response.data;
        if (users) {
          await UserCache.saveAll(users);
        }
      }
    } else {
      console.info("Users Cache hit");
    }
    res.status(200).send(users);
  } catch (err: any) {
    res.status(500).send({ error: err.message });
  }
});
 

In this endpoint, the following steps are performed:

  1. Attempt to fetch user data from the cache using UserCache.fetchAll().
  2. If the cache miss (data not found in the cache), make an API request to USERS_API using Axios.
  3. If the API request is successful, store the fetched user data in the cache using UserCache.saveAll(users).

Similarly, the /users/:id endpoint follows a similar caching pattern:

app.get("/users/:id", async (req: Request, res: Response) => {
  try {
    let user = await UserCache.fetchById(parseInt(req.params.id));
    if (!user) {
      console.info("User Cache miss");
      const response = await axios.get(`${USERS_API}/${req.params.id}`);
      if (response && response.data) {
        user = response.data;
        if (user) {
          await UserCache.save(user);
        }
      }
    } else {
      console.info("User Cache hit");
    }
    res.status(200).send(user);
  } catch (err: any) {
    res.status(500).send({ error: err.message });
  }
});
 

The Cache Logic

The caching logic itself is defined in the UserCache module. This module provides methods for saving and fetching user data in and from the Redis cache:

async function save(user: User) {
  return setJson(
    getKeyForId(user.id),
    { ...user },
    addMillisToCurrentDate(caching.contentCacheDuration)
  );
}
 
async function fetchById(userId: number) {
  return getJson<User>(getKeyForId(userId));
}
 
async function fetchAll() {
  return getJson<User[]>(getKeyForAll());
}
 
async function saveAll(users: User[]) {
  return setJson(
    getKeyForAll(),
    { ...users },
    addMillisToCurrentDate(caching.contentCacheDuration)
  );
}
 
export default {
  save,
  fetchById,
  fetchAll,
  saveAll,
};
 

These methods utilize the setJson and getJson functions, which abstract the process of storing and retrieving JSON data in the Redis cache.

export async function setJson(
  key: Key | DynamicKeyType,
  value: Record<string, unknown>,
  expireAt: Date | null = null
) {
  const json = JSON.stringify(value);
  return await setValue(key, json, expireAt);
}
 
export async function getJson<T>(key: Key | DynamicKeyType) {
  const type = await cache.type(key);
  if (type !== TYPES.STRING) return null;
 
  const json = await getValue(key);
  if (json) return JSON.parse(json) as T;
 
  return null;
}

Code repository

The complete code for this example is available on GitHub.

[Source code] (https://github.com/priya-jain-dev/redis/tree/main/redis-cache)

Wrapping Up

By implementing Redis caching in your Node.js application, you can significantly enhance its performance by reducing the load on external services and database queries. Caching is a powerful technique that improves response times and ensures a smoother user experience. The provided code example demonstrates how to integrate Redis caching with an API that fetches user data. Remember that caching strategies should be carefully designed to strike a balance between data freshness and efficiency, and they should be tailored to your specific application's requirements.

Next Steps