Practically Painless Caching with NestJS and Redis

By Joe Holmes

NestJS is a powerful backend framework for building enterprise-grade applications. Using a strongly typed, modular approach to file structure, it encourages developers to write cleaner, more maintainable code, and makes scaling much easier.

In this tutorial, we'll walk through installing a simple Redis cache with MongoDB in a NestJS application. With minimal setup, we can dramatically reduce our loading times-- a testament to the enhanced developer experience NestJS offers.

In this tutorial, we'll walk through a caching integration in NestJS, using a fast and flexible Redis database wired up to MongoDB. NestJS will automatically save our data to the Redis cache, guaranteeing lightning-fast response times. The tutorial will assume little familiarity with NestJS, and will generally attempt to be beginner-friendly.

I try not to choose backend frameworks on the basis of their logos, but with NestJS, whose logo appears to be a roaring housecat, I just couldn't help myself. With so many cats, it's hard not to be a fan.

There are cats all over this landing page.

Whether or not you're a fellow cat lover, if you'd like to wrap your mind around the NestJS framework it helps to think like a cat.

Cats, we know, like to sit in boxes. And NestJS likes boxes too: it encourages your code to sit in pre-defined boxes of neatly organized Typescript files, helping your codebase stay clean, modular, and maintainable. If the business logic of your enterprise-grade application was a cat, it would love to leap into the snug, orderly spaces of Nest. It would feel safe, reliable, and ready for a surge in traffic.

Requirements

Knowledge of NestJS is helpful, but not required.

A brief tour of our starter repo

We'll begin this tutorial with a NestJS REST API wired up to a MongoDB database. You can find the repo here.

In the interest of time, we won't walk through every line of code in this repo, but will instead provide a high-level overview that will prepare us for our Redis integration. If you're looking for a more exhaustive introduction, I recommend the free course on MongoDB and NestJS by Academind.

Let's begin by cloning into the starter repo and installing our dependencies.

 git clone https://github.com/bathrobe/nestjs-redis-starter.git
 cd nestjs-redis-starter
 npm install

Open up the repo in your IDE and let's check out what's inside. We'll be working within the src folder for the entirety of the tutorial.

Protip

If you're using VSCode, I recommend installing the Material Icons extension, which provides the helpful color-coding of the files seen here.

Remember the 'cats love boxes' analogy from earlier? Here it is in action. Our app has two 'boxes', one for books and one for our main app. These correspond to the routes of our API: the files beginning with app. pertain to our home route ("/"), while the files beginning with books. correspond to our books route ("/books").

Several filenames show up in both routes. These files are the basic building blocks of NestJS: services, modules, and controllers.

Modules, represented by the red A in the screenshot, connect all the files in that route into a single hub. If you open your app.module.ts file, you can see its basic structure.

//app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from "@nestjs/mongoose"
import { BooksModule } from './books/books.module';
 
@Module({
  imports: [
    BooksModule,
    MongooseModule.forRoot("YOUR_MONGO_URI_HERE"),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

In our imports, we bring in the BooksModule as well as a module for Mongoose (a helpful client for MongoDB). The controllers and providers come from our app.controller.ts and app.service.ts files.

In the controller file, we handle what happens with each kind of request, as well as with any subroutes. Below we see the syntax to a get request: a TypeScript decorator is declared, and then a function is defined that refers to a method in the appService.

//app.controller.ts
//...
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

The controller, then, reaches into the service file and injects the code that runs when the get request is called. This is where Nest ensures we keep our app clean and readable: while it might be tempting to write the code we find in the services file within our controller file, that would make the controller file much longer. As a result, our codebase would be harder to maintain.

Instead, our services file (the yellow A in the screenshot above) injects the code into our controller, which means it can remain safely abstracted away into its own file.

//in app.services.ts
//...
@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

Our books route obeys the same logic. The controller defines how we'll handle our requests, abstracting out the particulars to the services file. Both those files are then imported into the module file, which acts as the main hub that connects the module to the rest of the app.

The only addition in the books folder is the existence of a books.model.ts file, which defines the schema we send to MongoDB. We import that schema in our books.module.ts file and then use Mongoose's no-hassle methods to manipulate our Mongo data.

Ready to boot this thing up and start playing with our API? Let's get our Mongo instance connected and we'll be clear for takeoff.

Connecting our MongoDB database

Log into MongoDB Atlas and select "Create New Cluster". While it boots up, be sure to allow access to your IP address in "Security > Network Access."

Select "Connect Cluster".

Then "Connect your application."

Then copy your connection string! This will be what connects app.module.ts to our cluster.

Now, in your app.module.ts file, add the connection string to your Mongoose client, adding your username, password, and database name. If you plan to keep your code in a public repository, you should put this connection string in an environment variable instead.

//app.module.ts
@Module({
  imports: [
    BooksModule,
    MongooseModule.forRoot("PASTE_YOUR_CONNECTION_STRING_HERE"),
    //don't forget to add your username, password, and database name
  ],

Let's test it out and start our development server. Run npm run start:dev and take a look.

If all went well, you'll see a beautiful field of green:

Testing load times in Postman

To have a hands-on example of the benefits of our Redis cache, we'll first log the time it takes to fetch our data straight from MongoDB. We'll do this using Postman.

First, we need to create some data. Open Postman and navigate to your books route (by default, http://localhost:3000/books). Select a POST request from the dropdown, and add the body of a book as a JSON payload containing the title, the author, and the year it was published.

At localhost:3000/books:

As you can see, our API is configured to return the auto-generated ID when the POST is complete. By default, Nest sets the successful POST's status to 201.

Make a few more books, then change the request to GET.

Let's take careful note of the response time: 79ms. On my machine, after an initial, slightly laggy request, my books are returned somewhere between 70ms and 100ms each time. We can GET a single book by copying the id and adding it to our books route as a parameter. The response time is more or less the same.

82ms. Not bad, but not great. Now let's learn how to drastically reduce these times using Redis as a cache.

Creating a Redis image in Docker

In this tutorial, we'll be deploying our Redis server from Docker. If you haven't already, install the Docker Desktop app. (Fellow Windows users may need to enable virtualization in their BIOS in order to successfully install).

With Docker installed, we can create a Redis image via the command line with docker pull redis. You can confirm the image was created by opening the Docker desktop app.

Now run the image with docker run --name yourContainerName -p yourPortNumber:6379 -d redis. For instance:

You should be able to see it working from within the Docker desktop.

You can also confirm it works (as well as do some other cool tasks) by entering the Redis CLI with docker exec -it yourContainerName redis-cli.

And just like that, we've got a Redis instance installed, ready to use its blistering fast key-value storage to hold our data. Let's head back to our NestJS app and get it ready to integrate.

Register the CacheModule

We'll begin by installing cache-manager, a handy node package that abstracts away the more complicated parts of caching. We'll also install support for Typescript and Redis.

npm install cache-manager
npm install -D @types/cache-manager
npm install cache-manager-redis-store --save

Now let's head back into our books.module.ts file to import the cache system.

//books/books.module.ts

import { Module, CacheModule, CacheInterceptor } from '@nestjs/common';
import * as redisStore from 'cache-manager-redis-store';
import { MongooseModule } from "@nestjs/mongoose"
import { BooksController } from "./books.controller"
import { BooksService } from "./books.service"
import { BookSchema } from "./books.model"
import {APP_INTERCEPTOR} from '@nestjs/core';


@Module({
imports: [ 
    CacheModule.register({
        store:redisStore,
        host: 'localhost',
        port: 5003,
        ttl:300
    }), 
    MongooseModule.forFeature([{name: 'Book', schema: BookSchema}])],
controllers: [BooksController],
providers: [
    {
        provide:APP_INTERCEPTOR,
        useClass: CacheInterceptor
    }, BooksService]
})
export class BooksModule {}

Notice that we're importing CacheModule and CacheInterceptor from NestJS itself, and not the npm package we just installed. That's because Nest comes with built-in support for caching, making the rest of this process much more straightforward than it might be otherwise.

We also provide an APP_INTERCEPTOR that will automagically run caching logic on incoming requests.

Inject the CacheInterceptor in our controller file

Now that we've imported our cache into the books module, we need to go into our controller and inject it. NestJS explains the process in their docs.

First, import useInterceptors and CacheInterceptor from @nestjs/common, so that the import in your books.controller.ts file looks like this:

//books/books.controller.ts

import { Param, Patch, Controller, UseInterceptors, CacheInterceptor, Post, Body, Get, Delete } from "@nestjs/common"

Finally, right under the controller decorator, we'll tell Nest to use the CacheInterceptor.

//after the imports in books/books.controller.ts

@Controller('books')
@UseInterceptors(CacheInterceptor)
export class BooksController {
// ...the rest of our controller file, just as it was before!


Nest now automatically applies the cache to every request in this file.

Let's test it out in Postman.

Enjoying our newly cached data

Back in Postman, send another GET request to http://localhost:3000/books. Your first request will likely be no faster than the original ones – that's normal! The cache kicks in after the data is fetched for the first time. After your first request finishes, hit the 'send' button again. You should receive a lightning-fast reply.

Nest now returns my GET request in 18ms. That's really fast! (In this experiment, my GET requests averaged between 8ms-45ms load times).

Getting a single book by its ID also returns much faster, clocking in at 21ms.

Amazing! We can now perform more complex manipulations of data without dampening our users' experience.

Conclusion

Congratulations! You are the new owner of a turbo-charged, enterprise-grade server. We have only scratched the surface of what these tools are capable of, but I hope it has served as a welcoming introduction.

From here, you could experiment with Redis's capabilities as a microservice, hook up your API to a frontend, or deploy your Nest server to the public. Have fun!

Sanity.io: Get the most out of your content

Sanity.io is a platform to build websites and applications. It comes with great APIs that let you treat content like data. Give your team exactly what they need to edit and publish their content with the customizable Sanity Studio. Get real-time collaboration out of the box. Sanity.io comes with a hosted datastore for JSON documents, query languages like GROQ and GraphQL, CDNs, on-demand asset transformations, presentation agnostic rich text, plugins, and much more.

Don't compromise on developer experience. Join thousands of developers and trusted companies and power your content with Sanity.io. Free to get started, pay-as-you-go on all plans.

Other guides by author