NestJs Caching With Redis

The ultimate guide to implementing caching in NestJs with Cache Interceptor, Cache Manager and Redis

authorVladimir Agaev
-
May 13, 2022
thumbnail

Congratulations! You have deployed a NestJs application that is gaining in traction! A lot of users are using your app, the traffic goes viral.

At some point, you receive emails with complaints that your website is slow. You’ve probably heard that caching can solve the problem but you are unsure how to implement it.

You came to the right place!

In this article, I will explain what is caching, why you need it and how to implement it in your NestJs application.

Before we start, please note that you can find the github repository with the completed project here

What is caching?

Caching is a fairly old technique designed to improve your application’s performance and reliability.

Caching involves saving frequently requested data in an intermediary store called the "cache store" in order to avoid unnecessary calls to the primary database.

An HTTP request asking for data that is cached by the server will receive it directly from the cache store as opposed to getting it from a database. Which is much faster!

graph1

Why do you need caching?

Any web application that has some success will eventually run into bottlenecks. The most common bottleneck is usually related to how information is fetched from a primary database, like Postgres or MySQL.

Indeed, as the number of users grows so does the number of HTTP requests made to the server. This results in the same data being fetched all over again and again. It becomes important to optimise your application for speed and efficiency by caching frequently requested data.

Since most relational databases involve structured data, they are optimised for reliability and not for speed. That means that the data that they store is stored on disk, which is maginatude of times slower than the RAM. Using a NoSQL database does not bring any tremendous performance gains either.

The solution is to use an in-memory cache store.

In this tutorial, we are going to implement caching in NestJs and ultimately scale it with Redis, a fast in-memory database that is perfect for this use-case.

Pre-requisites

  • A NestJs starter project ready
  • Node version 16 or greater
  • Docker

Add an in-memory cache using the NestJs Cache Module

We will start by implementing the in-memory cache manager provided by NestJs, it will save the cache into the server's RAM. Once ready, we will transition to Redis for a more scalable caching solution.

The NestJs CacheModule is included in the @nestjs/common package. You will need to add it to your app.module.ts file.

app.module.ts

import { CacheModule, Module } from "@nestjs/common";
import { AppController } from "./app.controller";

@Module({
  imports: [
    CacheModule.register({
      isGlobal: true,
    }),
  ],
  controllers: [AppController],
})
export class AppModule {}

Note that we declare the module as global with isGlobal set to true. This way we don't need to re-import the caching module if we want to use it in a specific service or controller.

The Cache Module handles a lot of cache configuration for us and we will customize it later. Let's just point out that we can use caching with two different approaches:

  • The Interceptor approach
  • The Cache Manager approach with dependency injection

Let's briefly go through the pros and cons of each of them

When to use Interceptor vs Cache Manager in NestJs?

The intereceptor approach is cleaner but the cache manager approach gives you more flexibility with a bit of overhead.

As a rule of thumb, you will use the Cache Interceptor If you need an endpoint to return cached data from the primary database in a traditional CRUD app.

However, if you need more control or do not necessarily want to return cached data, you will use the cache manager service as a dependency injection.

So to summarise...

You would use the Cache Manager if you need more control, like:

  • Deleting from cache
  • Updating cache
  • Manually fetching data from the cache store
  • A combination of the above 👆🏻

To give a practical example, if you need to get a list of posts and you have an endpoint that fetches that list from the database, use a cache interceptor.

For anything more complex, the cache manager will be required.

Caching in NestJs using the Cache Interceptor

Let’s start with the interceptor as it allows to auto-cache responses from your API. You can apply the cache interceptor to any endpoint that you want to cache.

We'll create an src/utils.ts file that will store a getter function with a small timeout, to simulate some database delay.

utils.ts

// gets an array of dogs after 1 second delay
export function getDogs() {
  return new Promise((resolve, _) => {
    setTimeout(() => {
      resolve([
        {
          id: 1,
          name: "Luna",
          breed: "Caucasian Shepherd",
        },
        {
          id: 2,
          name: "Ralph",
          breed: "Husky",
        },
      ]);
    }, 1000);
  });
}

Now that we have a getter function for our dogs, we can use it in the app.controller.ts

app.controller.ts

import { Controller, Get } from "@nestjs/common";
import { getDogs } from "./utils";

@Controller()
export class AppController {
  @Get("dogs")
  getDogs() {
    return getDogs();
  }
}

Let's add some cache! Adding caching with interceptors is as simple as this 👇🏻

app.controller.ts

import {
  CacheInterceptor,
  Controller,
  Get,
  UseInterceptors,
} from "@nestjs/common";
import { getDogs } from "./utils";

@Controller()
export class AppController {
  @UseInterceptors(CacheInterceptor)
  @Get("dogs")
  getDogs() {
    return getDogs();
  }
}

Note that you can apply caching at the controller level by moving the @UseInterceptors(CacheInterceptor) above the @Controller() decorator. However, caching should be used in specific parts of your application. So it's usually better to apply it sporadically, at the endpoint level.

It's time to make a request now! I will use curl but you can use any http client of your choice.

You see that the first request takes approximately 1 second, while the second one takes 16 milliseconds. This is because the second request gets the array directly from the cache.

This is the power of caching! When applied on specific endpoints that are requested a lot, it can greatly accelerate your application.

# first request
time curl http://localhost:3333/dogs
[{"id":1,"name":"Luna","breed":"Caucasian Shepherd"},{"id":2,"name":"Ralph","breed":"Husky"}]curl http://localhost:3333/dogs  0.00s user 0.01s system 1% cpu 1.024 total

# second request
time curl http://localhost:3333/dogs
[{"id":1,"name":"Luna","breed":"Caucasian Shepherd"},{"id":2,"name":"Ralph","breed":"Husky"}]curl http://localhost:3333/dogs  0.00s user 0.01s system 51% cpu 0.018 total

The cache has an expiration date so you don't serve stale data to your users, the default value is 5 seconds but you can of course change that.

Let's change the expiration date to 10 seconds.

Customize TTL with CacheTTL decorator

Changing the cache TTL (time-to-live) can be done very easily with the built-in CacheTTL decorator.

app.controller.ts

import {
  CacheInterceptor,
  Controller,
  Get,
  UseInterceptors,
  CacheTTL,
} from "@nestjs/common";
import { getDogs } from "./utils";

@Controller()
export class AppController {
  @UseInterceptors(CacheInterceptor)
  @CacheTTL(10)
  @Get("dogs")
  getDogs() {
    return getDogs();
  }
}

Let's also add a custom cache key (which is by default the name of the endpoint)

import {
  CacheInterceptor,
  Controller,
  Get,
  UseInterceptors,
  CacheTTL,
  CacheKey,
} from "@nestjs/common";
import { getDogs } from "./utils";

@Controller()
export class AppController {
  @UseInterceptors(CacheInterceptor)
  @CacheTTL(10)
  @CacheKey("all-dogs")
  @Get("dogs")
  getDogs() {
    return getDogs();
  }
}

This allows us to have a greater control over how caching works!

While very practical, this approach does not allow us to manually delete from cache or update certain elements. While you might not need it in most cases, you will sometimes need to have more control over how data is saved to your cache store.

Now let's see how to use the cache manager!

Caching with the cache manager in NestJs

In order to use the cache in your services you need to inject it as a dependency. For that to work you need to import the cache-manager package.

npm install cache-manager
npm install -D @types/cache-manager

To avoid modifying our existing logic let's add another getter function to get some cats! 😺

utils.ts

export function getDogs() {
  return new Promise((resolve, _) => {
    setTimeout(() => {
      resolve([
        {
          id: 1,
          name: "Luna",
          breed: "Caucasian Shepherd",
        },
        {
          id: 2,
          name: "Ralph",
          breed: "Husky",
        },
      ]);
    }, 1000);
  });
}

export function getCats() {
  return new Promise((resolve, _) => {
    setTimeout(() => {
      resolve([
        {
          id: 1,
          name: "Vas",
          breed: "moggie",
        },
        {
          id: 2,
          name: "Clover",
          breed: "Blue Russian",
        },
      ]);
    }, 1000);
  });
}

And update our app.controller.ts with an additional endpoint to get the array of cats. This endpoint will use the cache-manager.

app.controller.ts

import {
  CacheInterceptor,
  CacheKey,
  CacheTTL,
  CACHE_MANAGER,
  Controller,
  Get,
  Inject,
  UseInterceptors,
} from "@nestjs/common";
import { Cache } from "cache-manager";
import { getCats, getDogs } from "./utils";

@Controller()
export class AppController {
  constructor(
    @Inject(CACHE_MANAGER)
    private cacheManager: Cache
  ) {}

  @UseInterceptors(CacheInterceptor)
  @CacheTTL(10)
  @CacheKey("all-dogsdogs")
  @Get("dogs")
  getDogs() {
    return getDogs();
  }

  @Get("cats")
  async getCats() {
    const cachedCats = await this.cacheManager.get(
      "all-cats"
    );
    if (cachedCats) return cachedCats;

    const cats = await getCats();
    this.cacheManager.set("all-cats", cats, {
      ttl: 10,
    });

    return cats;
  }
}

Note that the cache manager is injected in the constructor with the token CACHE_MANAGER. The cache manager gives us more control over how we get and return fetched data at the expense of a bit of code complexity.

In code above we try to get the cats array from the cache with the key all-cats.

If the cached value exists we immediately return it, otherwise we call getCats (in production that would be a call to your database), and save the fetched data in the cache with a ttl of 10 seconds.

That's it!

The cache manager also exposes:

  • del() - to delete a specific key-value cache record
  • update() - to update a specific key-value cache record
  • reset() - to delete the whole cache store (you would probably never want to use that!)

Our application is now cached and can sustain a great load, however there are some limitations...

  • Our cache does not scale past 1 node process, so we can't run our app in cluster
  • We can't run our app on different servers either
  • The cache is stored in the server's RAM, there is a possibility for the server to run out of memory

To fix that, we need to outsource our cache store to an in-memory database that is very fast and performant. Meet Redis ❤️

ebook

🎉 Get it for free!

Session based authentication is a critical part of most applications! Sadly NestJs documentation is rather light on the subject. This ebook is here to change that!

Discover how you can leverage sessions in your NestJs application and scale them like a pro with Redis database.

  • checkLearn about Sessions and why you need them
  • checkLearn about Authentication and Authorization
  • checkLearn about Cookies
  • checkImplement Sessions from scratch
  • checkLearn how to scale your session store with Redis

Add Redis cache store to NestJs

In order to switch our cache store from the server's RAM to Redis we need to import one extra library.

npm i cache-manager-redis-store redis
npm i -D @types/cache-manager-redis-store

This is a small library that uses the node-redis (unfortunately it does not support ioredis) under the hood. Integrating it to our app is very simple and can be done in the app.module.ts file.

We also need to add redis to get the RedisClientOptions type, not mandatory but a nice to have.

app.module.ts

import { CacheModule, Module } from "@nestjs/common";
import * as redisStore from "cache-manager-redis-store";
import type { RedisClientOptions } from "redis";
import { AppController } from "./app.controller";

@Module({
  imports: [
    CacheModule.register<RedisClientOptions>({
      isGlobal: true,
      store: redisStore,
      url: "redis://localhost:6379",
    }),
  ],
  controllers: [AppController],
})
export class AppModule {}

The last thing to add is the redis database. The easiest way to install it is through docker-compose! We can spawn a redis database and redis-commander using Docker thanks to a docker-compose.yml file.

Redis commander is similar to pgadmin or mysql workbench, but for Redis. It will help us inspecting the contents of Redis as we run our application.

docker-compose.yml

version: "3.9"
services:
  redis:
    image: redis:6.0
    ports:
      - 6379:6379
  redis-commander:
    container_name: redis-commander
    hostname: redis-commander
    image: ghcr.io/joeferner/redis-commander:latest
    environment:
      - REDIS_HOSTS=local:redis:6379
    ports:
      - "8081:8081"

To start the redis database you need to use docker compose in your terminal.

docker compose up -d

If you type docker ps in terminal you should get something like:

CONTAINER ID   IMAGE                                      COMMAND                  CREATED         STATUS                   PORTS                    NAMES
d03bde299aee   ghcr.io/joeferner/redis-commander:latest   "/usr/bin/dumb-init …"   6 minutes ago   Up 6 minutes (healthy)   0.0.0.0:8081->8081/tcp   redis-commander
b7b3ffd5c98a   redis:6.0                                  "docker-entrypoint.s…"   6 minutes ago   Up 6 minutes             0.0.0.0:6379->6379/tcp   nestjs-caching-tutorial-redis-1

Now, do a curl request curl http://localhost:3333/dogs and navigate to localhost:8081

Redis commander will show that our dogs are saved in cache, with (in my example) a time to live of 8 seconds before the cache is cleared

graph1

Summary

Well done if you've read so far! I hope that you found this content useful and educational.

In this tutorial you have learned:

  • What is caching and why you need to implement it
  • How to use Cache Interceptor in NestJs
  • How to use cache manager in NestJs
  • How to scale your cache store with Redis