Download as pdf or txt
Download as pdf or txt
You are on page 1of 15

NestJs Caching With Redis

The ultimate guide to implementing caching in NestJs with Cache


Interceptor, Cache Manager and Redis

Photo by Jandira Sonnendeck on Unsplash

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

At some point, you receive emails complaining 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 caching, why you need it and how to implement it in your
NestJs application.

What is Caching?
Before we start, please note that you can find the github repository with the completed
project

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” to avoid unnecessary calls to the primary database.

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

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. Optimizing
your application for speed and efficiency is important by caching frequently requested
data.
Since most relational databases involve structured data, they are optimised for reliability
and not for speed. That means the data they store on a disk is many 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 will 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 interceptor approach is cleaner, but the cache manager approach gives you more
flexibility with some 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 dependency injection.

So to summarise…

You will 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. You need to 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 you 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 to 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 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 greater control over how caching works!

While very practical, this approach does not allow us to delete from cache or update
certain elements manually. While you might not need it in most cases, you will sometimes
need 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


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 the 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 one node process, so we can’t run our app in the
cluster

We can’t run our app on different servers either.

The cache is stored in the server’s RAM, so 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 ❤️

Curious how to use Redis with sessions? I have a free ebook


that you can grab 👇
NestJs Session Authentication | The ultimate guide to implementing
session authentication in NestJs
Session based authentication is a critical part of most applications! Sadly
NestJs documentation is rather light on the…
www.codewithvlad.com
Add Redis cache store to NestJs
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 small library uses the node-redis (unfortunately, it does not support ioredis) under the
hood. Integrating it into 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
inspect 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 must use docker-compose in your terminal.

docker compose up -d

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

Redis commander will show that our dogs are saved in the cache, with (in my example) a
time to live of 8 seconds before the cache is cleared.
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 do 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

PS: Want to become an expert with NestJs? Get notified when my NestJs Essentials
course is released here
Code with Vlad | Learn NestJs from the best courses online
Write NestJs like an ExpertBuild reliably, with confidence Bootstrap your
journey with this complete deep dive into…
www.codewithvlad.com

Originally published at https://www.codewithvlad.com.

You might also like