Server-Side Caching in Node.js with Redis

This article provides a step-by-step guide on how to implement server-side caching in a Node.js application using Redis. You will learn how to set up a connection to a Redis server, write helper functions to get and set cached data, and create reusable middleware to intercept API requests and serve cached responses, significantly reducing database load and response times.

Why Use Redis for Caching?

Redis is an in-memory, key-value data store known for its extreme speed. By storing the results of expensive database queries or API calls in Redis, your Node.js application can retrieve this data in milliseconds instead of querying the primary database repeatedly.

Step 1: Install Dependencies

To get started, you need to install the official Redis client and Express (if you are building a web API) in your Node.js project. Run the following command in your terminal:

npm install redis express

Ensure you also have a running Redis instance. You can run Redis locally via Docker using docker run -p 6379:6379 -d redis.

Step 2: Initialize the Redis Client

Create a file named cache.js to initialize and export the Redis client connection. The modern redis package in Node.js uses Promises, which allows the use of async/await.

import { createClient } from 'redis';

const redisClient = createClient({
    url: 'redis://localhost:6379'
});

redisClient.on('error', (err) => console.error('Redis Client Error', err));

await redisClient.connect();

export default redisClient;

Step 3: Implement Basic Caching Logic

To cache data, you query Redis first using a unique key. If the data exists (a “cache hit”), you return it immediately. If it does not exist (a “cache miss”), you fetch the data from your database, store it in Redis with an expiration time (Time-To-Live or TTL), and then return it to the user.

Here is a basic example of this logic inside an Express route:

import express from 'express';
import redisClient from './cache.js';

const app = express();

app.get('/data', async (req, res) => {
    const cacheKey = 'expensive_data_key';

    try {
        // Check if data exists in Redis cache
        const cachedData = await redisClient.get(cacheKey);

        if (cachedData) {
            console.log('Cache hit!');
            return res.json(JSON.parse(cachedData));
        }

        console.log('Cache miss! Fetching from database...');
        // Simulate an expensive database query
        const freshData = await new Promise((resolve) => {
            setTimeout(() => resolve({ message: "Hello from the database!" }), 2000);
        });

        // Store the fresh data in Redis with a 1-hour expiration time (3600 seconds)
        await redisClient.set(cacheKey, JSON.stringify(freshData), {
            EX: 3600
        });

        return res.json(freshData);
    } catch (error) {
        console.error(error);
        return res.status(500).send('Server Error');
    }
});

app.listen(3000, () => console.log('Server running on port 3000'));

Step 4: Create Reusable Caching Middleware

Instead of writing caching logic inside every route, you can create a reusable Express middleware. This middleware will intercept requests, check the cache based on the request URL, and send the cached response if available.

import redisClient from './cache.js';

export const cacheMiddleware = (duration) => {
    return async (req, res, next) => {
        const key = `__express__${req.originalUrl || req.url}`;
        
        try {
            const cachedResponse = await redisClient.get(key);
            
            if (cachedResponse) {
                res.setHeader('Content-Type', 'application/json');
                return res.send(JSON.parse(cachedResponse));
            } else {
                // Temporarily override res.send to capture the response body
                res.originalSend = res.send;
                res.send = (body) => {
                    redisClient.set(key, JSON.stringify(body), {
                        EX: duration
                    });
                    res.originalSend(body);
                };
                next();
            }
        } catch (error) {
            console.error('Cache middleware error:', error);
            next(); // Proceed to route handler if cache fails
        }
    };
};

You can now apply this middleware directly to any route:

// This route will cache responses for 300 seconds (5 minutes)
app.get('/api/products', cacheMiddleware(300), async (req, res) => {
    const products = [{ id: 1, name: 'Laptop' }, { id: 2, name: 'Phone' }];
    res.json(products);
});

Step 5: Handling Cache Invalidation

Caching improves speed, but you must prevent users from seeing outdated data. Ensure you invalidate or delete keys when data is updated or deleted:

app.post('/api/products', async (req, res) => {
    // Save new product to database...
    
    // Clear the specific cached route
    await redisClient.del('__express__/api/products');
    
    res.status(201).send('Product added and cache cleared.');
});