# Cache

Caching is a great and simple technique that helps improve your app's performance. It acts as a temporary data store providing high performance data access.

Ts.ED provides a unified system caching by using the popular cache-manager (opens new window) Node.js module. Cache-manager provides various storage to cache content like Redis, MongoDB, etc... and multi caching!

By using the UseCache on endpoint methods or on service methods, you'll be able to cache the response returned by the Ts.ED server or the result returned by a Service.

# Configuration

Cache-manager module is already installed with the @tsed/common package (since v6.30.0). You just have to configure cache options and use the decorator to enable cache.

import {Configuration} from "@tsed/common";

@Configuration({
  cache: {
    ttl: 300, // default TTL
    store: "memory",
    prefix: "myPrefix" // to namespace all keys related to the cache
    // options options depending on the choosen storage type
  }
})
export class Server {}
1
2
3
4
5
6
7
8
9
10
11

# Store Engines

# Example with mongoose

import {Configuration} from "@tsed/common";
import mongoose from "mongoose";

const mongooseStore = require("cache-manager-mongoose");

@Configuration({
  cache: {
    ttl: 300, // default TTL
    store: mongooseStore,
    mongoose,
    modelOptions: {
      collection: "caches",
      versionKey: false
    }
  }
})
export class Server {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Sharing IORedis instance

import {Configuration, registerProvider} from "@tsed/di";
import {Logger} from "@tsed/logger";
import Redis from "ioredis";

export const REDIS_CONNECTION = Symbol("redis:connection");
export type REDIS_CONNECTION = Redis;

registerProvider({
  provide: REDIS_CONNECTION,
  deps: [Configuration, Logger],
  async useAsyncFactory(configuration: Configuration, logger: Logger) {
    const cacheSettings = configuration.get("cache");
    const redisSettings = configuration.get("redis");
    const connection = new Redis({...redisSettings, lazyConnect: true});

    cacheSettings.redisInstance = connection;

    try {
      await connection.connect();
      logger.info("Connected to redis database...");
    } catch (error) {
      logger.error({
        event: "REDIS_ERROR",
        error
      });
    }

    return connection;
  },
  hooks: {
    $onDestroy(connection: Redis) {
      return connection.disconnect();
    }
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

Then:

import {Configuration} from "@tsed/common";
import redisStore from "cache-manager-ioredis";

@Configuration({
  cache: {
    ttl: 300, // default TTL
    store: redisStore
  },
  redis: {
    port: 6379
  }
})
export class Server {}
1
2
3
4
5
6
7
8
9
10
11
12
13

TIP

This example works for a single redis connection. If you look for a complete example with Redis Cluster and Redis single connection, go to this example: https://gist.github.com/Romakita/432b1a8afaa726b41d0baf2456b205aa

# Interacting with the cache store

To interact with the cache manager instance, inject it to your class using the PlatformCache token, as follows:

@Injectable()
export class MyService {
  @Inject()
  cache: PlatformCache;
}
1
2
3
4
5

The get method on the PlatformCache instance is used to retrieve items from the cache.

const value = await this.cache.get("key");
1

To add an item to the cache, use the set method:

await this.cache.set("key", "value");
1

The default expiration time of the cache depends on the configured TTL on Server configuration level.

You can manually specify a TTL (expiration time) for this specific key, as follows:

await this.cache.set("key", "value", {ttl: 1000});
1

To disable the expiration of the cache, set the ttl configuration property to null:

await this.cache.set("key", "value", {ttl: null});
1

To remove an item from the cache, use the del method:

await this.cache.del("key");
1

To clear the entire cache, use the reset method:

await this.cache.reset();
1

# Cache response

To enable cache on endpoint, use UseCache decorator on a method as follows:

import {Controller, UseCache, Get, PathParams} from "@tsed/common";

@Controller("/my-path")
export class MyController {
  @Get("/:id")
  @UseCache()
  get(@PathParams("id") id: string) {
    return "something with  " + id;
  }
}
1
2
3
4
5
6
7
8
9
10

Note

UseCache will generate automatically a key based on the Verb and Uri of your route. If QueryParams and/or PathParams are used on the method, the key will be generated with them. According to our previous example, the generated key will be:

GET:my-path:1  // if the id is 1
GET:my-path:2  // etc...
1
2

WARNING

Only GET endpoints are cached. Also, HTTP server routes that use the native response object ( Res ) cannot use the PlatformCacheInterceptor .

# Cache a value

Because UseCache uses PlatformCacheInterceptor and not a middleware, you can also apply the decorator on any Service/Provider.

import {Injectable} from "@tsed/di";
import {UseCache} from "@tsed/platform-cache";

@Injectable()
export class MyService {
  @UseCache()
  get(id: string) {
    return "something with " + id;
  }
}
1
2
3
4
5
6
7
8
9
10

WARNING

node-cache-manager serialize all data as JSON object. It means, if you want to cache a complex data like an instance of class, you have to give extra parameters to the UseCache decorator. Ts.ED will use deserialize function based on the given type (and collectionType) to return the expected instance.

import {Injectable} from "@tsed/di";
import {UseCache} from "@tsed/platform-cache";

@Injectable()
export class MyService {
  @UseCache({type: MyClass})
  get(id: string): MyClass {
    return new MyClass({id});
  }

  @UseCache({type: MyClass, collectionType: Array})
  getAll(): MyClass[] {
    return [new MyClass({id: 1})];
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# Configure key resolver

By default, Ts.ED uses the request VERB & URL (in an HTTP app) or cache key (for other Service and Provider) to associate cache records with your endpoints. Nevertheless, sometimes you might want to set up the generated key based on different factors, for example, using HTTP headers (e.g. Authorization to properly identify profile endpoints).

There are two ways to do that. The first one is to configure it globally on the Server:

import {Configuration} from "@tsed/di";
import {PlatformContext} from "@tsed/common";

@Configuration({
  cache: {
    keyResolver(args: any[], $ctx?: PlatformContext): string {
      // NOTE $ctx is only available for endpoints
      return "key"
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11

The second way is to use the key option with UseCache decorator:

import {Controller, UseCache, Get, PathParams, PlatformContext} from "@tsed/common";

@Controller("/my-path")
export class MyController {
  @Get("/:id")
  @UseCache({key: "key"})
  get(@PathParams("id") id: string) {
    return "something with  " + id;
  }

  @Get("/:id")
  @UseCache({key: (args: any[], $ctx?: PlatformContext) => "key"})
  get(@PathParams("id") id: string) {
    return "something with  " + id;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Configure TTL

TTL can be defined per endpoint with UseCache :

import {Controller, UseCache, Get, PathParams, PlatformContext} from "@tsed/common";

@Controller("/my-path")
export class MyController {
  @Get("/:id")
  @UseCache({ttl: 500})
  get(@PathParams("id") id: string) {
    return "something with  " + id;
  }
}
1
2
3
4
5
6
7
8
9
10

# Define when a value can be cached 7.6.0+

Sometimes, you don't want to store in cache a value because isn't consistant to have it. For example, you can avoid caching data when the result is nullish:

import {Controller, UseCache, Get, PathParams, PlatformContext} from "@tsed/common";

@Controller("/my-path")
export class MyController {
  @Get("/:id")
  @UseCache({ttl: 500, canCache: "non-nullish"})
  get(@PathParams("id") id: string) {
    return null;
  }
}
1
2
3
4
5
6
7
8
9
10

In this case, the UseCache interceptor will ignore result that undefined or null.

You can also provide a custom function to ignore result:

import {Controller, UseCache, Get, PathParams, PlatformContext} from "@tsed/common";

@Controller("/my-path")
export class MyController {
  @Get("/:id")
  @UseCache({ttl: 500, canCache: (item: any) => item !== null})
  get(@PathParams("id") id: string) {
    return null;
  }
}
1
2
3
4
5
6
7
8
9
10

# Refresh cache keys in background 6.103.0+

The caching module support a mechanism to refresh expiring cache keys in background is you use UseCache on a Service method (not on controller method). This is done by adding a refreshThreshold option to the UseCache decorator.

If refreshThreshold is set and if the ttl method is available for the used store, after retrieving a value from cache TTL will be checked. If the remaining current ttl key is under the configured ttl - refreshThreshold, the system will spawn a background worker to update the value, following same rules as standard fetching.

const currentTTL = cache.ttl(key);

if (currentTTL < ttl - refreshThreshold) {
  refresh();
}
1
2
3
4
5

In the meantime, the system will return the old value until expiration.

import {Controller, UseCache, PathParams, PlatformContext} from "@tsed/common";

@Injectable()
export class MyService {
  @UseCache({ttl: 3600, refreshThreshold: 900})
  get(id: string) {
    return "something with " + id;
  }
}
1
2
3
4
5
6
7
8
9

In this example, the configured ttl is 1 hour and the threshold is 15 minutes. So, the key will be refreshed in background if current ttl is under 45 minutes.

# Refresh cached value 7.9.0+

A service method response can be cached by using the @UseCache decorator. Sometimes, we need to explicitly refresh the cached data, because the consumed data backend state has changed. By implementing a notifications service, the backend data can trigger an event to tell your API that the data has changed.

Here is short example:

import {Injectable} from "@tsed/di";
import {PlatformCache, UseCache} from "@tsed/platform-cache";
import {Controller, Get, PathParams, PlatformContext} from "@tsed/common";

@Injectable()
export class ProductsService {
  @Inject()
  protected pimClient: PimClient;

  @UseCache({ttl: 3600})
  async get(id: string) {
    return this.pimClient.get("/products/" + id);
  }
}

@Injectable()
export class NotificationsService {
  @Inject()
  protected cache: PlatformCache;

  @Inject()
  protected productsService: ProductsService;

  refreshProductId(id: string) {
    return this.cache.refresh(() => this.productsService.get(id));
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

This small example will force the data refresh.

TIP

If you have several cached method calls, then the refresh will also be done on all of these methods called by the function passed to PlatformCache.refresh().

# Multi caching

Cache-manager provides a way to use multiple caches. To use it, remove store option and use caches instead:

import {Configuration} from "@tsed/common";

@Configuration({
  cache: {
    ttl: 300, // default TTL
    caches: [memoryCache, someOtherCache]
    // options options depending on the choosen storage type
  }
})
export class Server {}
1
2
3
4
5
6
7
8
9
10

# Disable cache for test

It can sometimes be useful during unit tests to disable the cache. You can do this by setting the cache option to false:

describe("MyCtrl", () => {
  let request: SuperTest.Agent;
  beforeAll(
    TestMongooseContext.bootstrap(Server, {
      cache: false,
      mount: {
        "/rest": [MyCtrl]
      }
    })
  );
});
1
2
3
4
5
6
7
8
9
10
11

Last Updated: 10/5/2024, 7:24:49 PM

Other topics