Exploring Caching with NestJS and Redis
Tags: nestjs, redis, caching, typescript
Published: 2023-06-18 00:41:16

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](https://github.com/D-Lite/nest-cache-tutorial). 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.

```bash
npm install @nestjs/cache-manager cache-manager
```

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.

```ts
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 {}
```

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:

```ts
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 {}
```

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:

```ts
@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);  
}
```

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:

```ts
@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);  
}
```

* According to the official Nestjs documentation, the CacheModule will not work properly with GraphQL applications*

**Now Redis**

> According to [the official Redis website](https://redis.io/docs/about/#:~:text=Redis%20is%20an%20open%20source,%2C%20geospatial%20indexes%2C%20and%20streams.), “Redis is an open source, in-memory data structure store used as a database, cache, message broker, and streaming engine.”
> 
> Polled from [Redis](https://redis.io/)

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

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

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](https://github.com/node-cache-manager/node-cache-manager-redis-yet).

```ts
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 {}
```

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](https://redis.io/docs/stack/get-started/install/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:

```ts
@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);  
}
```

Let's take a brief look at the code block above:
```ts
const cachedData = await this.cacheService.get(contactId.toString());  
  if (cachedData) {  
    console.log('Getting data from cache');  
    return res.status(HttpStatus.OK).json(cachedData);  
  }  
```
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*.

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

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:

```bash
[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
}
```

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


![Redis on TablePlus](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/spagl1eehtyqtom6t0q4.png)


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.