How to optimize nodejs REST APIs using Cache

Abstract: Node.js excels in creating REST APIs with its non-blocking, speedy, and event-driven nature. However, for resource-intensive applications like e-commerce, social media, and video streaming, optimization is crucial. This article explores utilizing cache technology to enhance response time and boost performance in large-scale Node.js REST APIs.

By Javascript Diary

July 10, 2023

Introduction:

Due to its fantastic features, Node.js is the best choice for creating REST APIs. It is non-blocking, quick, and event-driven. It can handle several requests at once without experiencing any hiccups or outages, but it obviously needs optimisation for large applications like e-commerce online portals, social media with millions of users, video streaming, etc. This article will teach us how to use cache technology to improve the response time of our REST APIs.

Before we start, let us quickly learn about cache terminologies.

A cache refers to a temporary storage location that stores data or frequently accessed resources to provide faster retrieval in subsequent requests. It acts as a middle layer between the client and the server, storing previously generated or fetched data. When a client requests the same data again, it is retrieved from the cache instead of fetching it from the original source, resulting in improved response times and reduced server load. Caching can significantly enhance the performance and scalability of web applications.

While caching can bring significant performance improvements to REST APIs, there are a few potential disadvantages to consider:

1. Data Consistency: Caching introduces the challenge of maintaining data consistency. If the cached data becomes stale or outdated, it can lead to serving inaccurate information to clients. Strategies like cache invalidation or setting proper expiration times need to be implemented to address this issue.

2. Increased Complexity: Caching adds an additional layer of complexity to the system. It requires careful management of cache keys, cache invalidation logic, and handling cache failures. This complexity can lead to potential bugs or errors if not implemented and maintained properly.

3. Memory Overhead: Caching requires allocating memory resources to store cached data. In large-scale applications with high traffic, caching large amounts of data can result in increased memory usage, potentially impacting the overall system performance.

4. Cache Invalidation Challenges: Keeping the cache in sync with the underlying data source can be challenging. When data is updated or modified, ensuring that the corresponding cache entries are invalidated or updated in a timely and accurate manner can be complex, especially in distributed or clustered environments.

Despite these challenges, with careful planning and implementation, caching can significantly enhance the performance and scalability of REST APIs.

Implementing Cache in Node.js

I am considering here that Nodejs API are built and deployed to AWS serverless Lambda

We will see three ways to implement cache in our Node.js application.

1. Using Redis
2. Using node-cache
3. Using AWS services like cloudfront and ElastiCache

1. Redis Cache

So, what is Redic cache? When it comes to Redis, Redis is short for Remote Dictionary Server. Redis is a caching system that works by temporarily storing information in a key-value data structure.

Redis cache is popular because it is available in almost all major programming languages. In addition,  it is open source, which means it is a well-supported system that is a brilliant way to speed up your website or application without incurring a high cost.

Steps to integrate with your Nodejs application

Set up a Redis Server:

Install Redis on a server or a cloud instance of your choice. You can follow Redis’ official documentation for instructions on how to install Redis on your desired platform.

Configure Redis based on your requirements, including settings related to memory usage, persistence, networking, and security.

Connect to Redis in your Node.js application:

(i)- Install the redis package by running npm install redis.
(ii)- In your Node.js application, create a Redis client instance and connect it to your Redis server. Here’s an example:

				
					const redis = require('redis');
const redisClient = redis.createClient({
  host: 'your-redis-host',
  port: 'your-redis-port',
  // Add any additional Redis configuration options as needed
});

redisClient.on('error', (err) => {
  console.error('Redis error:', err);
});

				
			
Host: The host represents the network address or hostname of the machine where your Redis server is running. It can be an IP address (e.g., ‘127.0.0.1’) or a domain name (e.g., ‘example.com’). If Redis is running on the same machine as your Node.js application, you can use ‘localhost’ or ‘127.0.0.1’ as the host.

Port: The port is the network port number on which your Redis server is listening for incoming connections. By default, Redis uses port 6379. However, you can configure Redis to listen on a different port if needed.
It’s important to specify the correct port number when connecting to the Redis server.

(iii)- Use Redis for caching in your application:

Implement caching logic in your Node.js application using the Redis client. You can use the Redis client’s methods, such as get, set, and expire, to store and retrieve data from the Redis cache.

Here’s an example of caching data using Redis in an Express route:

				
					app.get('/cached-route', (req, res) => {
  const cacheKey = req.originalUrl;

  redisClient.get(cacheKey, (err, cachedData) => {
    if (err) {
      console.error('Redis error:', err);
      // Handle error
    } else if (cachedData) {
      res.send(cachedData);
    } else {
      // Fetch data from the backend
      const data = fetchDataFromBackend();

      // Store data in Redis cache
      redisClient.set(cacheKey, data);
      redisClient.expire(cacheKey, 3600);

      res.send(data);
    }
  });
});

				
			

2. Using "node-cache"

One popular caching module for Node.js is node-cache, which provides an in-memory caching mechanism. Here’s an example of how you can implement caching for specific routes using ‘node-cache’:

  1. Install the node-cache module by running npm install node-cache.
  2. Require the node-cache module and create a cache instance:
				
					const NodeCache = require('node-cache');
const cache = new NodeCache();

				
			
3. Create a caching middleware function:
				
					function cacheMiddleware(req, res, next) {
  const cacheKey = req.originalUrl; // Use the request URL as the cache key
  const cachedData = cache.get(cacheKey);

  if (cachedData) {
    res.send(cachedData); // Send the cached response
  } else {
    // Data not in cache, proceed to the next middleware or route handler
    next();
  }
}
				
			
4. Apply the caching middleware to specific routes:
				
					const express = require('express');
const app = express();

// Apply caching middleware to a specific route
app.get('/cached-route', cacheMiddleware, (req, res) => {
  // Fetch the data from the backend
  const data = fetchDataFromBackend();

  // Store the fetched data in the cache
  cache.set(req.originalUrl, data, 3600);

  res.send(data);
});

				
			

With this approach, the caching middleware checks if the requested data is available in the in-memory cache (node-cache) and serves it directly. If the data is not in the cache, it proceeds to the next middleware or route handler to fetch the data from the backend and stores it in the cache for subsequent requests.

Keep in mind that node-cache is an in-memory cache, which means that the cached data will be stored within the memory of your Node.js application. If your application is scaled across multiple instances or restarts, the cache will be lost. If you need a distributed cache or more advanced features, you may consider using a dedicated caching service like Amazon ElastiCache or other similar solutions.

3. Using AWS services like cloudfront and ElastiCache

Let us consider you have built your node apis and deployed it on AWS serverless(eg: lambda). Now the quick way to implement cache for your backend is  to use AWS services like Cloudfront or ElastiCache. 

(i) Amazon ElastiCache (Redis): You can use Amazon ElastiCache, a fully managed Redis service, to implement caching in your Node.js application. ElastiCache provides an in-memory data store that can significantly improve response times. You can configure your application to first check the cache for the requested data and only hit the backend if the data is not available in the cache. Redis has various features that allow you to manage and control the caching behavior effectively.

Here’s an example of how you can implement caching for specific routes using Amazon ElastiCache (Redis) and Express:

Set up the ElastiCache (Redis) connection:

i) Ensure you have an ElastiCache Redis cluster deployed and accessible.
ii) Install the Redis client for Node.js by running npm install redis.
iii) Create a Redis client instance and connect to your ElastiCache Redis cluster.

Here’s an example of how you can establish a connection:

				
					const redis = require('redis');
const redisClient = redis.createClient({
  host: 'your-redis-host',
  port: 'your-redis-port',
  // Add any additional Redis configuration options as needed
});

redisClient.on('error', (err) => {
  console.error('Redis error:', err);
});

				
			

In the code snippet above, ‘your-redis-host’ and ‘your-redis-port’ should be replaced with the actual host and port of your ElastiCache Redis cluster. These values can be obtained from the ElastiCache management console or programmatically.

Create a caching middleware function:

(i) – Define a middleware function that checks if the requested data is available in the cache (ElastiCache Redis).
(ii) – If the data is present in the cache, retrieve it and send the response immediately.
(iii) – If the data is not in the cache, continue to the next middleware or route handler to fetch the data from the backend and store it in the cache.

Here’s an example of a caching middleware function:

				
					function cacheMiddleware(req, res, next) {
  const cacheKey = req.originalUrl; // Use the request URL as the cache key

  redisClient.get(cacheKey, (err, cachedData) => {
    if (err) {
      console.error('Redis error:', err);
      next(); // Proceed to the next middleware or route handler
    } else if (cachedData) {
      res.send(cachedData); // Send the cached response
    } else {
      // Data not in cache, proceed to the next middleware or route handler
      next();
    }
  });
}

				
			

Apply the caching middleware to specific routes:

(i)- Use the caching middleware function on the routes you want to cache.
(ii)- Place the middleware before the route handler to check the cache before processing the request.

Here’s an example of how to apply the caching middleware to a specific route:

				
					const express = require('express');
const app = express();

// Apply caching middleware to a specific route
app.get('/cached-route', cacheMiddleware, (req, res) => {
  // Fetch the data from the backend
  const data = fetchDataFromBackend();

  // Store the fetched data in the cache
  redisClient.setex(req.originalUrl, 3600, data);

  res.send(data);
});

				
			

(ii) Amazon CloudFront: Another option is to use Amazon CloudFront, a content delivery network (CDN), which can cache and deliver your API responses from edge locations around the world. CloudFront acts as a caching layer between your users and your Lambda function. By configuring CloudFront with appropriate caching settings, you can reduce the number of requests reaching your Lambda function, thereby improving response times.

Both options have their advantages and can be used based on your specific requirements.

If you require more granular control over caching behavior or need features specific to Redis, such as data persistence, pub/sub messaging, or complex data structures, using Amazon ElastiCache with Redis would be a good choice.

On the other hand, if you are primarily looking to cache static content or have a larger distribution network with global users, Amazon CloudFront can be a better fit.

Consider the specific needs of your application, such as data size, caching requirements, data volatility, and cost implications, to make an informed decision about which caching solution to choose.

Conclusion:

In this blog, we explored the topic of caching in Node.js applications. We discussed the use of Redis and in-memory caching libraries like node-cache. We also delved into how to connect to AWS ElastiCache for Redis. Additionally, we covered caching specific Express routes and the process of clearing the cache. Considering scalability, features, and integration, we gained insights into effectively leveraging caching to improve application performance.