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.
Top comments (6)
thank you so much, this saved me some time
again wanted to thank you :)
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.
I could not figure out where your cacheService is coming from
I don't know either, but I figured it out with this one:
deps: