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
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 {}
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 {}
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);
}
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);
}
- 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
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 {}
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);
}
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);
}
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);
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
}
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:
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.
