Introduction & Project Setup
Hey there. Today we're going to look at implementing caching in Nest.js. Caching is an important feature that helps us improve our app performance by acting as a temporary data store to provide high-performance data access. So this tutorial will walk through all of the caching features in Nest.js. I'll also show you how to implement a Redis store and then finally I'll show you how to implement unit tests around this caching.
To get started, I'm going to use the Nest CLI to generate a new project. We'll call it sjs-cache and then I'll go ahead and use pnpm as my package manager. So I'm going to go ahead and cd into the Nest.js cache project, and we need to go ahead and install a couple packages. We'll install cache-manager and I'm also going to go ahead and install a development dependency here: types-cache-manager, so we get types for this project. After we've installed these, we can run pnpm start:dev to go ahead and start our API.
Configuring the Cache Module
So I've gone ahead and opened the project up in VS Code. Let's go ahead and open up our app.module. And this is where we actually bootstrap the Cache Module. So in our imports right here, all we have to do is call CacheModule.register(). Out of the box here, we don't need to provide any options to register. However, we can actually provide an options object here and there's a number of options we can configure.
For example, we can provide TTL globally for our cache, which is the amount of time that items in our cache are going to last for before they're evicted. So by default, this will be five seconds, but if we wanted to raise it, we could give a global default of 60 seconds. And then we can also specify the number of items that can be in cache at once, a thousand if we wanted a thousand items before the cache was emptied. So I'm going to go ahead and leave the defaults here. We can also say isGlobal here and set it to true. This will make the module globally available so we don't need to import it in other modules.
Finally in this register call, this is where we can configure different stores to use. So by default, this cache module is going to use an in-memory store, but we can also specify a different store like Redis, and we'll do that later on.
Manual Caching with the CacheManager
So there are a few different ways we can implement caching in our application. The first of which is to actually get access to the cache manager directly. So let's go ahead and take a look at that in our app controller. We have our default route here for getHello. So in our app service, we're calling getHello at the default get route.
So in here, in this app service, let's go ahead and actually inject the cache manager so we can interact with it. So we're going to have private readonly cacheManager and this is going to be of type Cache, which we can import from cache-manager. Now importantly, we also have to use the Inject decorator here from NestJS and then provide it with the token CACHE_MANAGER. So this is necessary to inject this properly.
And now that we have access to the cache manager, we can directly set and get items in our in-memory store. So to do this, we'll actually need to make our function here asynchronous. We can remove our return type. And now if we want to set an item on the cache, we can call this.cacheManager.set and then we provide the key, cache-item for a key. And then we can provide a value, and this can be anything. It can be a primitive like an integer, a string, or it can be an entire object here. So we could have an actual object with a key and values. So really we have no limitations of what we can cache.
And then in order to get things from the cache, we can say cachedItem's a new const here await this.cacheManager.get. We'll get that cached item that we just cached, and then we'll go ahead and log out the cached item so we can see it here. So I also go back to our app controller and change this route to async and get rid of the return type here. So make sure your application is running here and then go ahead and open up Postman. We'll go ahead and execute a GET request for localhost:3000, which is where our API is running. Let's go ahead and trigger this, and we get our default "Hello World" response. But now if we go back and look at our application logs, we can see the cached item being logged out here, which is that key of 32 object.
So in addition, if we go back to our app service, we can also provide an options object here when we set the cached item. And this has the same options that that global register method has that we called here. So we can specify any TTL for this item, give it a different TTL from the default, so that it will expire sooner or longer than the default that we specify in the register method.
So lastly, there are a few other additional functions on this cache manager. You can call this.cacheManager.delete and specify we want to delete the cached item as soon as we set it. So when we log it out here, we should actually find that it's now undefined. So we can see our logs here, it's undefined because we deleted it. And lastly, if we want to delete everything inside of the cache, we can call reset, and this will delete everything inside of the cache all at once.
Automatic Caching with Interceptors & Decorators
So now that we've looked at interacting with the cache manager directly, we can also implement caching on the route level. So in a controller, we can actually decorate the entire controller with UseInterceptors and let's use the CacheInterceptor here from NestJS common. So what this is going to do is, by default, it's going to cache all of our GET routes only, and it will cache them based on the defaults we specified in our cache module. So that we don't actually even trigger the code in our routes here, we're just going to return the cached response from the original call. So to see this in action, we'll take a look at our logs here. So if we send off one request really quickly and then another one right after, we should only see one undefined in the logs. And that's because we're only triggering our route handler code once because we're caching the response and have no need to go back to it. So let's send off two quick requests: one, two. Now if you look at our logs, we can see we only have one undefined here coming from our log statement in our app service here because we only hit this code one time. If we got rid of this interceptor here and did the same thing—one request, two requests—we see we get two undefined. So you can see the power of this caching in that we don't have to go back down to our code and take time to respond to the client. We can just send back these cached responses.
So if you wanted to implement this on every controller at once, it's pretty easy to do. We just need to go back to our app module, and in our providers array here, let's go ahead and provide an APP_INTERCEPTOR, which is a global interceptor. And then we'll use class CacheInterceptor here from NestJS common. Now this app interceptor, the CacheInterceptor will be applied to every controller in our system, and we'll have caching out of the box very easily.
Now if you want to actually fine-tune the cache controls on a route basis, you can also do this very easily by using some decorators here. So you can specify a custom key for any given route by using the CacheKey decorator. So we can give any key we want here. And we can also specify a custom TTL here. So I want to give this a TTL of 60 seconds. So now if I go back to Postman, send off one, two, three, four, five requests, I should only expect to see one undefined because we have cached the response. And in the logs, that's exactly what we see.
Unit Testing Cache-Dependent Services
Finally, before we move on to the Redis store, let's see how we would actually mock this cache manager dependency in your tests. So let's go ahead and create a new app.service.spec. And in here, we're going to have everything in a describe block. And so we want to describe the AppService and then we'll set a let here called appService set equal to the AppService. Okay, and now beforeEach, we can go ahead and actually create our testing module. So let's get a handle to our module ref by calling await Test.createTestingModule. And we're going to have an empty imports right here. We're going to have no controllers, and then we're going to have our providers array. So obviously we're going to instantiate our AppService here. And then we need to mock our cache manager, which our app service depends upon. So in order to do that, we will use an object here, and we'll specify we want to provide the CACHE_MANAGER token, which is how we inject it in the app service, and then I'm going to say useValue here and we're going to create a mock cache manager.
So let's go ahead and make this asynchronous, and let's define that mock cache manager up here. This is going to be an object here. Remember in the app.service we're going to mock each of these functions we call on the cache manager. So we have set, del, reset, and get. So each of these is going to be a Jest function that we're mocking. So now we've successfully mocked our cache manager. We can call compile on our module ref here and then we'll set our appService equal to moduleRef.get. Pass in the AppService and then we'll pass in the AppService name here.
Finally for our actual test, we will say it should be defined to specify that the app service should now be defined and we don't have any errors when we compile. So we'll expect appService here to be defined. And if all goes well, we should see that our app service is indeed defined. So we can open up a new terminal and we can run pnpm test app.service, and we should see that our app service is indeed defined and we have correctly created all of our dependencies mocked.
Using Redis as a Distributed Cache Store
So finally, let's see how we can implement Redis into this caching system so that if we have our application running in a distributed environment, we have a central place where all of our cache is managed.
So you need to make sure that you actually have Redis running on your system. If you have Mac like me, you can install Redis by using Brew, Homebrew. So brew install redis and then we can run brew services start redis to start our Redis store. Now in order to integrate this into our app, we're going to install another package called cache-manager-redis-store.
And now let's go back into our app.module, inside of our CacheModule.register function, we're going to provide that options object again. And now we're going to provide a Redis store. So to do this, we'll go ahead and add a new import up here and say import * as redisStore from 'cache-manager-redis-store'. Now inside of this object here, we can provide a store and set the store equal to redisStore. Then we can go and provide a socket object here, which is information on how to connect to this specific store. So we can give it a host here in our case localhost with a port of 6379. So now we've successfully connected to our Redis store.
Let's see this in action. If you remember in our app.service, we're logging out this cached item. However, we've reset the store, so we should expect to see null here when we log this out.
Previously when we restarted our application, we would lose all cached values because it lives in memory. But now that we're using a Redis store to store this information, it should persist after application restarts. So let's test this out by sending off a few requests in Postman. If you remember, since we're caching our response, we should only see one console log. So we'll send off a few requests here and see one null log statement. Now if we restart the application, we should expect to still see no console logs because our response is still stored in the Redis store. So now I'm going to send a few requests and you can see we don't have anything in the logs here because we've gone to the Redis store for the response and we never had to call appService.getHello. So we didn't log anything out, and we saved time on this request thanks to Redis.
Conclusion
So I hope this short explanation of caching in Nest was useful. I plan on releasing a lot more videos around Nest.js, Kubernetes in the future, so if that interests you, be sure to subscribe. Thanks, and I'll see you next time.