DEV Community

Cover image for Exploring Caching with NestJS and Redis
Sagacité
Sagacité

Posted on

Exploring Caching with NestJS and Redis

In this tutorial, we will be exploring the world of caching, with NestJS and Redis.

Before we dive head-in into Redis, NestJS provides its in-memory cache, that can serve as an in-memory data store, but can easily be switched to high-performant caching system like Redis.

To get started, you can fork this github repository. Please, ensure you select the base branch as it contains the base application setup for this tutorial.

After setting up you base application setup, you can proceed to install the cache-manager package and its dependencies.



npm install @nestjs/cache-manager cache-manager


Enter fullscreen mode Exit fullscreen mode

In case you are familiar with NestJS in-memory cache implementation, there is a current update to the cache-manager, we are using version 5 throughout this tutorial. As opposed to the version 4, the version 5 now provides ttl (Time-To-Live) in milliseconds instead. We have to do the conversion before parsing it to NestJS as NestJs does not do the conversion for us.

To enable caching, we have to import the CacheModule into the app.module.ts file.



import { Module } from '@nestjs/common';  
import { AppController } from './app.controller';  
import { AppService } from './app.service';  
import { MongooseModule } from '@nestjs/mongoose';  
import { ContactModule } from './contact/contact.module';  
import { CacheModule } from '@nestjs/cache-manager';  

@Module({  
  imports: [  
  // mongodb://127.0.0.1:27017/nest-redis - use env variables or other secure options in production
    MongooseModule.forRoot('mongodb://127.0.0.1:27017/nest-redis', {  
      useNewUrlParser: true,  
    }),    ContactModule,  
    CacheModule.register(),  
  ],  controllers: [AppController],  
  providers: [AppService],  
})  
export class AppModule {}


Enter fullscreen mode Exit fullscreen mode

Our app.module.ts file should look like the one above.

We are also going to import it into our Contact module. With any feature module in NestJS, there is a need to import the cache-manager into them respectively. If you have a module that does not require caching, then it is not a necessity.

Our contact.module.ts should be similar to the code block below:



import { Module } from '@nestjs/common';  
import { ContactService } from './contact.service';  
import { ContactController } from './contact.controller';  
import { MongooseModule } from '@nestjs/mongoose';  
import { ContactSchema } from './schemas/schema';  
import { CacheModule } from '@nestjs/cache-manager';  

@Module({  
  imports: [  
    MongooseModule.forFeature([{ name: 'Contact', schema: ContactSchema }]),  
    CacheModule.register(),  
  ],  providers: [ContactService],  
  controllers: [ContactController],  
})  
export class ContactModule {}


Enter fullscreen mode Exit fullscreen mode

A good alternative, is setting the isGlobal option for the CacheModule.register(). This option can be considered if you need caching availability across all your modules.

Let's proceed to our Contact controller to modify some of our existing endpoints. For the @Get('contacts/:contactId') endpoint, we want to check if our response is in cache, before calling the method. This is achievable using the @useInterceptors decorator:

Our getContact method should be similar to this below:



@UseInterceptors(CacheInterceptor) // Automatically cache the response for this endpoint  
@Get('contacts/:contactId')  
async getContact(  
  @Res() res,  
  @Param('contactId') contactId,  
) {  
  const contact = await this.contactService.getContact(contactId);  
  if (!contact) throw new NotFoundException('Contact does not exist');  
  return res.status(HttpStatus.OK).json(contact);  
}


Enter fullscreen mode Exit fullscreen mode

Things to note:

  • The expiry time is 5 seconds
  • The interceptor autogenerates the cache key for the cache entry based on the route path. Both options can be effectively controlled and overriden.

Implementing an override, our getContact will be:



@UseInterceptors(CacheInterceptor) // Automatically cache the response for this endpoint  
@CacheKey('getcontact-key')  
@CacheTTL(60000) // now in milliseconds (1 minute === 60000)
@Get('contacts/:contactId')  
async getContact(  
  @Res() res,  
  @Param('contactId') contactId,  
) {  
  const contact = await this.contactService.getContact(contactId);  
  if (!contact) throw new NotFoundException('Contact does not exist');  
  return res.status(HttpStatus.OK).json(contact);  
}


Enter fullscreen mode Exit fullscreen mode
  • According to the official Nestjs documentation, the CacheModule will not work properly with GraphQL applications*

Now Redis

According to the official Redis website, “Redis is an open source, in-memory data structure store used as a database, cache, message broker, and streaming engine.”

Polled from Redis

To use Redis instead of the in-memory cache, we will need to install the relevant package:



npm i --save cache-manager-redis-yet


Enter fullscreen mode Exit fullscreen mode

This package is a nestjs wrapper for passing configurations to the node_redis package. We can now change some config of our app.module.ts to use Redis.

Note: cache-manager-redis-store is been discontinued to allow for the package we just installed. This package we installed is been tracked directly by the Nestjs team.



import { Module } from '@nestjs/common';  
import { AppController } from './app.controller';  
import { AppService } from './app.service';  
import { MongooseModule } from '@nestjs/mongoose';  
import { ContactModule } from './contact/contact.module';  
import { CacheModule } from '@nestjs/cache-manager';  
import { redisStore } from 'cache-manager-redis-yet';  

@Module({  
  imports: [  
    MongooseModule.forRoot('mongodb://127.0.0.1:27017/nest-redis', {  
      useNewUrlParser: true,  
    }),  
    CacheModule.registerAsync({  
      isGlobal: true,  
      useFactory: async () => ({  
        store: await redisStore({  
          socket: {  
            host: 'localhost',  
            port: 6379,  
          },        
        }),      
      }),    
    }),    
    ContactModule,  
  ],  controllers: [AppController],  
  providers: [AppService],  
})  
export class AppModule {}


Enter fullscreen mode Exit fullscreen mode

This has allowed us to add new config options like:
redisStore: represent the node-cache-manager-redis-yet we just installed
host and port are set to their default

Incase you don't have Redis server running yet, you can look up various installation methods for your operating system or consider the Docker option

Heading to our contact.controller page, we can configure our getContact method, to check if we have a cached data yet before querying the main database. If it exists, we want to return it else we want to set it after querying the DB.

Our getContact should be similar to this:



@Get('contacts/:contactId')  
async getContact(@Res() res, @Param('contactId') contactId) {  
  const cachedData = await this.cacheService.get(contactId.toString());  
  if (cachedData) {  
    console.log('Getting data from cache');  
    return res.status(HttpStatus.OK).json(cachedData);  
  }  
  const contact = await this.contactService.getContact(contactId);  
  if (!contact) throw new NotFoundException('Contact does not exist');  
  await this.cacheService.set(contactId.toString(), contact);  
  const newCachedData = await this.cacheService.get(contactId.toString());
  console.log('data set to cache', newCachedData);  
  return res.status(HttpStatus.OK).json(contact);  
}


Enter fullscreen mode Exit fullscreen mode

Let's take a brief look at the code block above:



const cachedData = await this.cacheService.get(contactId.toString());  
  if (cachedData) {  
    console.log('Getting data from cache');  
    return res.status(HttpStatus.OK).json(cachedData);  
  }  


Enter fullscreen mode Exit fullscreen mode

The cachedData variable is checking if we have an existing cache, if it exists, you can check your logger and you'll get Getting data from cache.



await this.cacheService.set(contactId.toString(), contact);  
  const newCachedData = await this.cacheService.get(contactId.toString());
  console.log('data set to cache', newCachedData);  


Enter fullscreen mode Exit fullscreen mode

If our data does not exist in cache, the codeblock above helps us to set in cache.

Your cached data will now persist to your local Redis server.

You can test out the endpoint and you should get a result similar to my output in your logger:



[Nest] 3720  - 05/09/2023, 10:00:49 AM     LOG [NestApplication] Nest application successfully started +5ms
data set to cache {
  _id: '6459510cfc398baa01998a66',
  first_name: 'Daniel',
  last_name: 'Olabemiwo',
  email: 'dee@gmail.com',
  phone: '+23832101',
  message: 'Welcome to this side',
  __v: 0
}


Enter fullscreen mode Exit fullscreen mode

You can confirm in your GUI of choice (I like TablePlus) by sending a request to your NestJS app where caching is used and you'll see the data is now persisted:

Redis on TablePlus

Congratulations, you have just added a Redis cache to your NestJS application.

In this tutorial, we have successfully added a Redis cache to our Nestjs application.
Redis enables lower application latency and very high data access. This allows software engineers to build highly performant, reliable solutions.

And that's it! What do you think? Let me know in the comments below.

Top comments (6)

Collapse
 
geeksamu profile image
Ahmed Abdelsalam

thank you so much, this saved me some time

Collapse
 
geeksamu profile image
Ahmed Abdelsalam

again wanted to thank you :)

Collapse
 
hiteshpathak profile image
Hitesh Pathak

Hey I'm alredy using a redisStore (using connect-redis) to store session (I use cookies for auth).


store: new RedisStore({ client: this.redis })

Here the above client is created using node_redis and imported here:

const redisClient = createClient({
socket: {
host: configService.get('REDIS_HOST'),
port: configService.get('REDIS_PORT'),
},
});

Here I see we initialize a new store using host and port, will that reuse the same redis client as the other store uses, I think it should? as it will be more efficient.

Or should I go about setting up this store in a different way. If anyone knows, please help me with some pointers. I can share more code, to figure things out.

Collapse
 
emmybritt profile image
Emmy britt

I could not figure out where your cacheService is coming from

Collapse
 
monarkha profile image
Mikita Monarkha • Edited

I don't know either, but I figured it out with this one:

import { CACHE_MANAGER, CacheStore } from '@nestjs/cache-manager';
// below in constructor
 constructor( @Inject(CACHE_MANAGER) private readonly cacheManager:CacheStore){}
  @Get()
  async testFunc() {
    await this.cacheManager.set('test', 'zxc');
    const value = await this.cacheManager.get('test');
// ...
Enter fullscreen mode Exit fullscreen mode
Collapse
 
monarkha profile image
Mikita Monarkha • Edited

deps:

"@types/cache-manager-redis-store": "^2.0.4",
"redis": "^4.6.11",
"cache-manager-redis-yet": "^4.1.2",|
"@nestjs/cache-manager": "^2.1.1",
Enter fullscreen mode Exit fullscreen mode