Caching in Strapi with Strapi Middleware Cache

Most firms strive to retrieve and store data faster. It is because no one wants an application that is slow or lags. When there's a lot of data in the database, it becomes challenging to fetch data from the database swiftly, which may cause your application to lag. To avoid this, most software firms cache data.

The idea of Caching aims to increase data retrieval performance by reducing the need to access the underlying slower storage layer. For Trading off capacity for speed, a cache typically stores a subset of data transiently, in contrast to databases whose data is usually complete and durable.

Strapi middleware cache is a middleware that caches incoming GET requests on the Strapi API based on query params and model ID. This middleware allows you to either cache your data on your application's memory by default or with Redis, a third-party database. The cache is automatically busted every time a PUT, POST, or DELETE request comes in.

This article will explore the Strapi middleware cache and how to cache data in our Strapi application.

Why is Caching Necessary

Just as I emphasized in my opening statement, time is of great essence when dealing with data. Most software stores their data in a database, which may have different storage layers. Because of the different layers of storage in the database and how large the data is, it becomes difficult to retrieve data faster.

Although users must have accessed the same information earlier, it doesn't get displayed more quickly when they re-access them.

To make fetching data easier, developers use a caching system. A caching system is a high-speed data storage layer, which stores a batch of data from the database.

Whenever a user retrieves information from the database, the data is stored in a cache to increase data retrieval performance instead of accessing data from the different storage layers whenever a user requests them.

The cache doesn't contain all of the data in the database, just a fraction of the data. In most cases, data previously accessed by the users.

Below are some importance of caching and why most software firms incorporate it into their application:

  1. Caching helps to improve the performance of an application since the retrieval of data is faster and more efficient.
  2. Caching reduces the load in your backend database as your application's frontend doesn't always have to send a request to your backend whenever a user requests data. It prevents poor performance or even crashes in times of spikes.
  3. Caching eliminates database hotspots that occur due to users frequently asking a subset of data.

Strapi Middleware Cache Installation and Setup

To use the Strapi middleware cache, I assume the following prerequisites:

  1. You should install the required version of Node.js in your working environment.
  2. You should understand the basic knowledge of how Strapi works.

First, you'll be installing the Strapi middleware cache plugin into your Strapi project by running the command below in your CLI from your project's location.

npm install --save strapi-middleware-cache
    #or
    yarn add strapi-middleware-cache

Once you have the plugin set up in your local environment, add it to your Strapi project. Create a Strapi project. If you don't have one running, add a middleware.js file to your project's configuration. To do this, create a middleware.js file in your config folder, which is at the root of your project.

Enable the middleware cache plugin by putting the code below into the middleware.js file you just created. You can configure environments by following the guide on Strapi's documentation.

module.exports = ({ env }) => ({
      settings: {
        cache: {
          enabled: true,
        }
      }
    });

Start your Strapi project with strapi develop to see that the middleware cache plugin has been set up successfully in your working environment.

$ strapi develop
    [2021-06-26T06:15:50.394Z] debug [Cache] Mounting LRU cache middleware
    [2021-06-26T06:15:50.396Z] debug [Cache] Storage engine: mem

Model Configuration

After setting up the middleware cache plugin with our Strapi, we need to explicitly tell the plugin which model we'll want to apply to the cache. Let's add models to the middleware configuration object.

module.exports = ({ env }) => ({
      settings: {
        cache: {
          enabled: true,
          models: ['blogs'],## you can add other models
        }
      }
    });

If you run your project with strapi develop, you’ll notice that caching for the blogs route has commenced.

$ strapi develop
    [2021-06-26T20:25:14.744Z] debug [Cache] Mounting LRU cache middleware
    [2021-06-26T20:25:14.745Z] debug [Cache] Storage engine: mem
    [2021-06-26T20:25:14.752Z] debug [Cache] Caching route /blogs/:id* [maxAge=3600000]

Storage Engine Configuration

Strapi middleware cache plugin uses memory by default to cache data. However, you can configure Redis as the cache engine. Sometimes, you'll want to configure the max number of entries, cache timeout, etc., in your storage engine.

To do this, you'll have to specify the needed configurations under settings in your middleware configuration object. For example, I can configure the type of engine for caching as Redis and set maxAge and other properties of our models.

module.exports = ({ env }) => ({
      settings: {
        cache: {
          enabled: true,
          type: 'redis',
          maxAge: 2600000,
          models: ['blogs'],
        }
      }
    });

Storage engine configuration can take the following properties:

Type
The type property describes the type of storage engine for the middleware cache plugin to use. By default, the type is set to memory which is denoted by mem.

Max
Max denotes the maximum number of entries that the cache can take in. By default, this number is set to 500.

Max Age
The maxAge tells us the time after which a cache entry will be considered invalid. This time is usually represented in milliseconds. The default maxAge in the strapi-middleware-cache is 3600000 milliseconds.

Cache Timeout
Sometimes, due to network problems caching data takes a longer time. Strapi-middleware-cache has a cacheTimeout which specifies the time after which a cache request is timed out.

Enable Etag Support
Etag (entity tag) headers are identifiers for a specific version of data. ETags prevents updates of a resource that could be simultaneous from overwriting each other. By default, the property enableEtagSupport is set to false.

Logs
Strapi-middleware-cache logs output in your console. For instance, when we first ran our project after configuring the middleware, we got the output below.

[2021-06-26T06:15:50.394Z] debug [Cache] Mounting LRU cache middleware
    [2021-06-26T06:15:50.396Z] debug [Cache] Storage engine: mem

By default, the property log is set to true. If you set the property to false, you won't get an output in your console concerning the middleware.

Populate Context
The property populateContext is set to false by default. If you set the property to true, this setting will inject a cache entry point into the Koa context. It is advantageous, especially for developers who are interested in building with Koa.

Headers
For individual model configuration, you can set headers (Cache-Control Header) to specify caching policies. For instance, we can set headers for our model to accept and cache JavaScript only.

{
        model: 'account',
        headers: ['accept-JavaScript']
      }

You can write different policies for your headers like no-cache, no-store, public and private.

Redis Config (redis only)
For developers who don't want to use the default storage engine: memory and opt for Redis, they can configure Redis with a config object passed to ioredis. For instance, I can configure other properties and configure Redis sentinel to monitor some of my nodes if there's a failover.

module.exports = ({ env }) => ({
      settings: {
        cache: {
          enabled: true,
          type: 'redis',
          maxAge: 2600000,
          max: 400,
          cacheTimeout: 400,
          enableEtagSupport: true,
          logs: true,
          populateContext: false,
          models: ['blos'],
          redisConfig: {
            sentinels: [
              { host: '192.168.10.41', port: 26379 },
              { host: '192.168.10.42', port: 26379 },
              { host: '192.168.10.43', port: 26379 },
            ],
            name: 'redis-primary',
          }
        //or you can connect to redis lab with the command below.
         redisConfig: {
            host: 'redis-5555.c8.us-east-1-4.ec2.cloud.redislabs.com',
            port: 5555,
            password: 'secret_password',
          },
        }
      }
    });

If you don't configure your storage engines, they'll take the default settings. The default settings for storage engines are shown below.

Configuring Each Model Separately

You can configure the properties of each model individually. For developers with a lot of models, this step is always helpful.

For instance, you'll want to configure the maxAge or cacheTimeout property individually since the models will be accessing different types of resources. To configure model properties individually, but the property of interest into the model property as shown below.

module.exports = ({ env }) => ({
      settings: {
        cache: {
          enabled: true,
          models: [
            {
              model: 'blogs',
              maxAge: 1000000,
              cacheTimeout: 400,
            },
            {
              model: 'posts',
              maxAge: 800000,
              cacheTimeout: 450,
            }
          ]
        }
      }
    });

From the example above, we have configured the maxAge and cacheTimeout properties for blogs and posts individually. Since we didn't configure other properties, blogs and posts will follow the default configuration for other properties.

We can also set other properties in the cache object, which will cause blogs and posts to take those configurations instead of the default configuration. For instance, from the example below, our storage engine for blogs and posts will be Redis, and we won't have log outputs in our console.

module.exports = ({ env }) => ({
      settings: {
        cache: {
          type: 'redis',
          logs: false,
          enabled: true,
          models: [
            {
              model: 'blogs',
              maxAge: 1000000,
              cacheTimeout: 400,
            },
            {
              model: 'posts',
              maxAge: 800000,
              cacheTimeout: 450,
            }
          ]
        }
      }
    });

Pluralization and Single Types

By default, when you create a content type in Strapi, your collection name is pluralized. For instance, if you create a blog content type, the collection name is blogs. What the Strapi-middleware-cache does is pluralize the model name in the configuration. So, if you put your model name as posts or post the middleware will cache /posts/*.

For Strapi single types like about, this is not beneficial and can lead to an error. Furthermore, to make the middleware skip pluralization for single types, set singleType to true for your model.

module.exports = ({ env }) => ({
      settings: {
        cache: {
          enabled: true,
          models: [
            {
              model: 'about',
              singleType: true,
            }
          ]
        }
      }
    });

Caching is essential, especially if you have an extensive database with different storage layers. Notice that with each PUT, POST, or DELETE request, the cache automatically gets busted.

Busting a cache is crucial because it enables updates to keep happening in the cache even when using it. So, when the user updates, deletes or adds new data to their model, the cache is updated accordingly.

Also, you can clear your cache by setting the populateContext configuration to true. This setting will give you direct access to the cache engine. To do this, we will add a controller directly under our middleware module object. The middleware will extend the Koa Context with an entry point to clear the cache from within controllers.

module.exports = ({ env }) => ({
      settings: {
        cache: {
          enabled: true,
          populateContext: true,
          models: ['blogs']
        }
      }
    });


    // controller

    module.exports = {
      async index(ctx) {
        ctx.middleware.cache.store // This will give a direct access to the cache engine
        await ctx.middleware.cache.bust({ model: 'blogs', id: '1' }); // Clear cache for this specific record
        await ctx.middleware.cache.bust({ model: 'posts' }); // Clear cache for the entire model collection
        await ctx.middleware.cache.bust({ model: 'about' }); // Don't pluralize model names of single types
      }
    };

Conclusion

In this article, we have explored caching in Strapi with the strapi-middleware cache. We first discussed caching and why it is necessary to add caching to your application. Next, we looked at the strapi-middleware cache and installed and set it up in our working environment. Then we explored the different configurations and properties for the strapi-middleware cache.

23