Decorator caching in NestJS with type-cacheable

darraghor

Darragh O'Riordan

Posted on March 11, 2020

Decorator caching in NestJS with type-cacheable

I needed to cache some data in a NestJS application. Nest provides an awesome module for caching responses from nest http or microservice responses from controllers. But this Nest caching module doesn't easily allow you to cache from any method using the decorators.

class MyService {
  // I wanted this: Cache whatever the output of the method is based on the key (id in this case)
    @Cacheable((args: any[]) => args[0], ttl:TtlSeconds.ONE_MINUTE)
    public get(id:number): SomeModel{
  }
}
Enter fullscreen mode Exit fullscreen mode

I integrated type-cacheable in to the project to get this functionality. Here are the steps...

Create a redis instance

There are many ways to setup redis locally or on the cloud. Search Google for more info. Once you have a redis instance running you can continue. On a mac try

brew install redis
Enter fullscreen mode Exit fullscreen mode

Add the connection parameters to your environment. I use REDIS_HOST, REDIS_PORT etc in this example. See the code below for more.

Add the packages we need

You need to add a redis client, ioredis works well for this. Add type-cachable and then add the types for ioredis because we’re using typescript.

yarn add ioredis type-cacheable
yarn add -D @types/ioredis
Enter fullscreen mode Exit fullscreen mode

Creating the caching module

You can add the functionality where ever you like but it is useful as a separate module where you can add it to your main module imports so it gets set up on application startup.

Otherwise I would add it to the main application module directly.

Here is the code Most of these classes or enums would be in their own files.

/*
This enum just makes it easy to set cache ttls. Type cacheable uses
seconds.
*/
export enum CacheTtlSeconds {
  ONE_MINUTE = 60,
  ONE_HOUR = 60 * 60,
  ONE_DAY = 60 * 60 * 24,
  ONE_WEEK = 7 * 24 * 60 * 60,
}
/*
This is just a generic exception we can throw and easily detect later in our app,
 in logs or other systems.
*/
export class NotCacheableException<T> extends Error {
  public constructor(message: string) {
    super(message)
  }
}
/*
This class maps env variables to a redis io config object
*/
@Injectable()
export class RedisCacheConfigurationMapper {
  public static map(): IORedis.RedisOptions {
    return {
      lazyConnect: true,
      host: process.env.REDIS_HOST,
      port: Number(process.env.REDIS_PORT),
      password: process.env.REDIS_PASSWORD,
      connectTimeout: Number(process.env.REDIS_TIMEOUT),
      tls: process.env.REDIS_USETLS === 'true' ? {} : undefined,
    }
  }
}

/*
This is where we setup the typecacheable store. We use Nest's OnModuleInit
interface to have the setup run immediately.
This allows us to stop application start if there is a problem
configuring our redis instance.
*/
@Injectable()
export class RedisCacheService implements OnModuleInit {
  private redisInstance: IORedis.Redis | undefined

  public async onModuleInit(): Promise<void> {
    try {
      if (this.isAlreadyConfigured()) {
        return
      }

      this.redisInstance = new IORedis(RedisCacheConfigurationMapper.map())
      // we set up error events. Note that we don't want to
      // stop the application on connection errors. We don't want the lack
      // of a working cache to break our application. You need to think
      // about if this is the correct approach for your application.
      this.redisInstance.on('error', (e: Error) => {
        this.handleError(e)
      })
      // This is where we configure type cachable to use this redis instance
      useIoRedisAdapter(this.redisInstance)
      // and finally we open the connection
      await this.redisInstance?.connect()
    } catch (e) {
      this.handleError(e as Error)
    }
  }

  private handleError(e: Error): void {
    console.error('Could not connect to Redis cache instance', e)
  }

  private isAlreadyConfigured(): boolean {
    return this.redisInstance !== undefined
  }
}
Enter fullscreen mode Exit fullscreen mode

How to use the decorators

You just add them to your method! It’s super easy and promotes having clean methods for the models you are caching. See below for an example of a common CRUD repository.

class MyService {
    @Cacheable((args: any[]) => args[0], ttl:TtlSeconds.ONE_MINUTE)
    public get(id:number): SomeModel{
    }

    @CacheClear((args: any[]) => (args[0] as SomeModel).id)
    public update(model:SomeModel): void{

    }

    @CacheClear((args: any[]) => args[0])
    public delete(id:number): void{

    }
}
Enter fullscreen mode Exit fullscreen mode

Using Nest CACHE_MANAGER

If you use Nest caching for http responses then you don’t really need to configure a second redis instance. You can just ask the dependency injection container for an instance of the internal cache manager and use that.

export class RedisCacheService implements OnModuleInit {
    public constructor(@Inject(CACHE_MANAGER) private readonly cache: ICacheManager){}

    ...
     useIoRedisAdapter(cache.store);
     ...
 }
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
darraghor
Darragh O'Riordan

Posted on March 11, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related