NextJs SSR caching using Redis

xvandev

Ivan Alekseev

Posted on July 22, 2024

NextJs SSR caching using Redis

Introduction

This is my first article, and I would like to explore the topic of caching using Redis and SSR (Server-Side Rendering). I’ll explain the importance of caching, provide an example of a basic implementation, and discuss some of the challenges I’ve faced in my projects. I will use NextJs since it has an out-of-the-box implementation of SSR.

Caching

Firstly, let’s discuss the significance of caching. Caching is crucial for enhancing the performance of your project and minimizing database requests. It plays a vital role in improving response times, reducing server load, and ensuring a smoother user experience.

If you are working on a public website, caching is an essential feature. Regardless of how fast your server and database are, they can become bottlenecks under high load.

Moreover, there is a direct dependency of site traffic on page loading speed due to Google taking this into account when indexing, putting faster sites in higher positions. That’s why caching and site performance are crucial for achieving success. In large projects, every millisecond counts because even slight delays can result in significant financial losses.

Caching types

There are several types of caching like API caching, HTML caching, CDN, and frontend caching(memoization, local storage, etc). Every project should take an exact set of tools according to their unique requirements, loads, and other features. It is crucial to choose an appropriate set of tools in order to not increase the project’s complexity and the costs of its maintenance.

In this article, I want to pick out exact API caching using Redis and SSR due to it helps to reduce server response which has a crucial role in optimization. It does not matter if your frontend code performs when the server slows down. In this way more often the client just closes the page and the site will go down in Google positions.

There are also tools like Google Page Insights and Light House which also give key importance to the server response speed.

Why do we need SSR?

Maybe some of you have a question — why do we need to make a request on the server? Why just don’t give an HTML template to the client and do all the jobs there — fetch the data and build the page? And I can say — yes, this is ok, but only when you have a website that does not require search engine indexing. Meanwhile, If you want to index your website and increase search traffic, your content must be included in the HTML returned by the server. Google favors websites with server-side rendered content over those that rely heavily on client-side content loading.

Why Redis?

Redis is my favorite tool because of its ease of use and flexibility. Among the many advantages of using Redis for caching, several stand out. It allows for dynamic state management, enabling efficient tracking and selective deletion of data. Redis also supports database migration to a separate server and facilitates sharding for optimized data distribution. Its capability for smart cache implementation, which I’ll elaborate on in my next article, is another key benefit. Additionally, Redis makes it easy to create backups, which is crucial for quickly restoring large databases.

So, let’s start implementing. Let’s assume you have already installed the Next application and there is some list of entities that you get from the server and you want to cache it.

1. Redis installation

You can run Redis on any server or install it directly on your computer. Personally, I prefer using Docker:

File: .docker-compose.yml

services:
  redis:
    image: 'bitnami/redis:latest'
    environment:
      - REDIS_PASSWORD=password
      - REDIS_AOF_ENABLED=no
    volumes:
      - "./docker/redis:/bitnami"
    ports:
      - "6379:6379"
  redisinsight:
    container_name: redisinsight
    image: redislabs/redisinsight
    ports:
      - "8081:8001"
Enter fullscreen mode Exit fullscreen mode

2. Redis setup

To begin, let’s create an env file to store our connection settings.

File: .env

REDIS_HOST=localhost
REDIS_PASSWORD=password
REDIS_PORT=6379
And set up Redis client.
Enter fullscreen mode Exit fullscreen mode

File: lib/redis.js

import Redis from "ioredis";

const options = {
    port: process.env.REDIS_PORT,
    host: process.env.REDIS_HOST,
    password: process.env.REDIS_PASSWORD
};

const client = new Redis(options);
export default client;
Enter fullscreen mode Exit fullscreen mode

3. API function

Next, we’ll develop a straightforward API that utilizes the Redis wrapper:

File: api/index.js

import catalog from "./catalog";

const config = {
    API_URL: 'http://localhost:3000/api/catalog'
}
export const Api = {
    catalog: catalog(config)
}
File: api/catalog.js

import {redisGetHandler} from "../utils/redis";

export default (config) => ({
    async getItems(Redis = null, reset = false) {
        return await redisGetHandler(Redis, config.API_URL, 'getItems', reset);
    }
});
Enter fullscreen mode Exit fullscreen mode

4. Redis handler function

Next, we need to create a handler function for the API that can both store and retrieve data from Redis.

File: utils/redis.js

import axios from "axios";

export async function redisGetHandler(Redis, apiPath, apiCode, reset = false) {
    const url = `${apiPath}/${apiCode}`;
    if (!Redis) {
        return await fetchData(url);
    }

    try {
        if (!reset) {
            const cachedData = await Redis.get(apiCode);
            if (cachedData) {
                return JSON.parse(cachedData);
            }
        }   
        const serverData = await fetchData(url);
        if (serverData) {
            Redis.set(apiCode, JSON.stringify(serverData));
        }
        return serverData;
    } catch(e) {
        console.log(e);
    }
    return await fetchData(url);
}

async function fetchData(url) {
    try {
        const {data} = await axios.get(url);
        return data;
    } catch (e) {
        console.log(e);
    }
    return null;
}
Enter fullscreen mode Exit fullscreen mode

5. Page component and getServerSideProps function

Let’s integrate API into the serverFunction. It is essential to use Redis in the server functions, as Redis is specifically designed to operate on a server. Failing to do so will result in an error on the client side.

File: pages/index.js

import {useEffect, useState} from "react";
import {Api} from "../api";
import Redis from "../lib/redis";

export default function Home({items: serverItems}) {
    const [clientItems, setClientItems] = useState([]);
    const [isClientItemsLoading, setIsClientItemsLoading] = useState(true);

    const getClientItems = async () => {
        setIsClientItemsLoading(true);
        setClientItems(await Api.catalog.getItems());
        setIsClientItemsLoading(false);
    }

    useEffect(() => {
        getClientItems();
    }, []);

    return (
        <div>
            <h3>Items from client</h3>
            {isClientItemsLoading && (<div>Loading...</div>)}
            {!isClientItemsLoading && <ul>
                {clientItems.map(item => <li key={item.id}>{item.name}</li>)}
            </ul>}
            <h3>Items from server</h3>
            <ul>
                {serverItems.map(item => <li key={item.id}>{item.name}</li>)}
            </ul>
        </div>
    );
}

export async function getServerSideProps({query}) {
    const reset = Boolean(query.reset);
    const items = await Api.catalog.getItems(Redis, reset)
    return {
        props: {
            items
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

6. Result

In summary, here’s the scheme: we have two API calls, one on the server and one on the client.

Image description

repository: https://github.com/xvandevx/blog-examples/tree/main/nestjs-cache

You might have noticed that our requests are cached only on the server. When the API is called from the client, Redis is not utilized because it doesn’t work on the client side. This was an issue I encountered in my project when I first set up caching. The reason is that using Redis directly on the client is unsafe, and there isn’t a front-end library that supports this functionality.

So, Next.JS can help us fix it. It allows us to create an internal API wrapper within our application. Here’s how we can proceed:

To implement it we just need a few changes in our code.

7. API function

We need to implement API_WRAPPER_URL and getItemsWrapper function.

File: api/index.js

import catalog from "./catalog";

const config = {
    API_URL: '<http://localhost:3000/api/catalog>',
    API_WRAPPER_URL: '<http://localhost:3000/api/apiWrapper>'
}
export const Api = {
    catalog: catalog(config)
}
File: api/catalog.js

import {redisGetHandler} from "../utils/redis";
import axios from "axios";

export default (config) => ({
    async getItems(Redis = null, reset = false) {
        return await redisGetHandler(Redis, config.API_URL, 'getItems', reset);
    },
    async getItemsWrapper(reset = false) {
        const {data} = await axios.get(`${config.API_WRAPPER_URL}/getItems?reset=${reset}`);
        return data;
    }
});
Enter fullscreen mode Exit fullscreen mode

8. Setup server API wrapper

This crucial step involves configuring the server API to act as a wrapper for your API endpoint.

File: pages/api/apiWrapper/getItems.js

import Redis from "/lib/redis";
import {Api} from "../../../api";

export default async function handler(req, res) {
    const data = await Api.catalog.getItems(Redis, req.query.reset === 'true');
    res.status(200).json(data);
}
Enter fullscreen mode Exit fullscreen mode

Another advantage is the ability to conceal your API endpoint URL using this wrapper, which enhances security.

9. Change API calls from external to the API wrapper

All you need to do is to change the call from Api.catalog.getItems() to Api.catalog.getItemsWrapper().

File: pages/index.js

import {useEffect, useState} from "react";
import {Api} from "../api";

export default function Home({items: serverItems}) {
    const [clientItems, setClientItems] = useState([]);
    const [isClientItemsLoading, setIsClientItemsLoading] = useState(true);
    const getClientItems = async () => {
        setIsClientItemsLoading(true);
        setClientItems(await Api.catalog.**getItemsWrapper**());
        setIsClientItemsLoading(false);
    };
    useEffect(() => {
        getClientItems();
    }, []);
    return (
        ....component
    );
}
export async function getServerSideProps({query}) {
    const reset = Boolean(query.reset);
    const items = await Api.catalog.getItemsWrapper(reset);
    return {
        props: {
            items
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Thus, no matter where we request from (server or client) — we always get data from Redis, which means we get it quickly and optimally.

Repository: https://github.com/xvandevx/blog-examples/tree/main/nestjs-cache-wrapper

Cache resetting

Resetting the cache is a complex topic, which I plan to delve into further in my upcoming article. Its implementation demands a thorough understanding of your project’s mechanics. Simply caching all API endpoints in Redis with a timeout won’t suffice. Certain projects, like online stores where real-time price updates are crucial, cannot afford discrepancies between cached data on detail pages and checkout prices. Even small, low-traffic stores would find such discrepancies disappointing for their customers.

Therefore, it’s essential to adopt appropriate caching strategies for each part of your application. In this article, I focus on scenarios where immediate cache updates aren’t critical — I simply define a cache lifetime and leave it at that.

In addition, I recommend implementing manual cache resetting for every project because it is helpful and easy to implement. From time to time you want to see the changed data on the page without waiting for the cache to time out. For this, I recommend using the URL parameter for cache resetting, for example ?reset_cache=y:

export async function getServerSideProps({query}) {
    const reset = Boolean(query.reset);
    const items = await Api.catalog.getItems(Redis, reset);
    return {
        props: {
            items
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

Content managers and testers will be very grateful to you for this!

And that’s it! Now we have cached API which allows us to increase performance and avoid heavy database requests every time you call API.

Recap

In this article, I considered the basic use case of Redis with NextJs, which is sufficient for small projects and is more suitable for learning purposes. Large-scale projects typically use more complex systems incorporating Redis, KeyDB, Memcached, HTML caching, CDNs, client caching, and other technologies. It is crucial to choose a set of tools that fit your particular project, at the same time so that it does not increase complexity and development speed.

If you are in a big company more than likely you don’t need to care about such things as caching and page load speed. More often this is the responsibility of a separate team or it is handled by the backend side or you are doing something where cache is not required at all. Nevertheless, in my point of view, every front-end developer should be aware of how it works and how to deal with it.

Moreover, in our rapidly changing time when AI comes to our lives, the demand for full-stack developers will start to grow again as it was 15 years ago. That is why knowledge like this will help to point you out from the other developers on the market.

💖 💪 🙅 🚩
xvandev
Ivan Alekseev

Posted on July 22, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related