Demo

Adding Caching

Declare caches with @Cache on the root module (in-memory or Redis from env), inject with @CacheClient, and optionally cache HTTP routes or method results with @CacheRoute / @CacheFn. Reuse another plugin’s cache instead of re-declaring it.

Goal & when

Use caching for repeated reads, session/token stores, and expensive computations. Declare the client once on the owning plugin’s root module, then inject it anywhere in the same process — including other plugins.

  • In-memory — single-process, simple TTL / checkperiod
  • Redis — shared across instances; host / port / password from env
  • Cross-plugin — inject the owner’s cache; do not re-declare @Cache for the same client

1. Prerequisites

You need a root @Module and, for Redis, reachable credentials from PluginEnv / base .env.

  • Root @Module({ name: metadata.name, … }) exists
  • Redis — REDIS_HOST / REDIS_PORT / REDIS_PASSWORD (or your chosen env names) available at boot
  • If consuming another plugin’s cache, list that owner in pluginDependencies
  • @Cache must sit on the @Module class — not on services or controllers

2. Declare @Cache on the root module

Pass a unique name when the plugin opens more than one cache. Multiple @Cache decorators on the same module class are allowed.

  • type — "in-memory" or Redis options (host, port, password, …)
  • name — optional client id; required when one plugin declares multiple @Cache entries
  • Owner is the loading plugin — consumers address it by that plugin’s metadata.name
TSXdeclare caches
1import { Cache, Module } from "@quan-erp/shared-backend-core"; 2import metadata from "../../module.metadata.json" with { type: "json" }; 3 4@Cache({ 5 name: "publisher", 6 type: "in-memory", 7 checkperiod: 1000, 8}) 9@Cache({ 10 name: "session", 11 type: "redis", 12 host: process.env.REDIS_HOST, 13 port: Number(process.env.REDIS_PORT ?? 6379), 14 password: process.env.REDIS_PASSWORD, 15}) 16@Module({ 17 name: metadata.name, 18 providers: [...], 19 controllers: [...], 20}) 21export class MyPluginModule {}

3. Inject with @CacheClient

Inject an ICache on a service property. The first argument is the plugin that owns the cache; the second is the optional client name.

  • metadata.name — this plugin’s own cache (add name when it has several)
  • "other-plugin" — a cache created by another plugin
  • CacheManager.DEFAULT_PLUGIN — core / shared default cache owner when applicable
TSXinject
1import { 2 CacheClient, 3 Service, 4} from "@quan-erp/shared-backend-core"; 5import type { ICache } from "@quan-erp/shared-backend-core"; 6import metadata from "../../module.metadata.json" with { type: "json" }; 7 8@Service() 9export class SessionService { 10 @CacheClient(metadata.name, "session") 11 private session: ICache; 12 13 async getToken(userId: string) { 14 return this.session.get(`token:${userId}`); 15 } 16}

4. Route & method helpers

Point helpers at the same plugin + name as the owning @Cache. Use delete helpers when writes invalidate cached reads.

  • @CacheRoute / @DeleteCacheRoute — HTTP response cache; key may be string or (req) => string
  • @CacheFn / @DeleteCacheFn — method result cache; key is (...args) => string
  • plugin + name must match the owning @Cache (including another plugin’s client)
TSroute cache
1@CacheRoute({ 2 key: (req) => `orders:${req.query.status ?? "all"}`, 3 plugin: metadata.name, 4 name: "session", 5}) 6@Get("/orders") 7list() { /* … */ }
TSmethod cache
1@CacheFn({ 2 key: (id: number) => `order:${id}`, 3 plugin: metadata.name, 4 name: "session", 5}) 6async findOne(id: number) { /* … */ }

5. Cross-plugin reuse

Caches live in a process-wide registry keyed by owner plugin (+ optional name). A consumer does not re-declare @Cache for that client — it injects the owner’s cache and points helpers at the same plugin + name.

  • Owner — declares @Cache({ name, … }) and boots first (pluginDependencies)
  • Consumer — @CacheClient("owner-plugin", "cache-name")
  • Route / method helpers — same owner plugin + name
  • Do not hard-code a second identical @Cache in every consumer
TSconsumer inject
1@Service() 2export class ReportService { 3 // "warehouse" plugin declared @Cache({ name: "replica", ... }) 4 @CacheClient("warehouse", "replica") 5 private replica: ICache; 6}

6. Verify

  • Plugin boots without cache / Redis connection errors
  • Injected ICache get / set / del behaves as expected
  • Cached routes return the same payload until invalidated
  • @DeleteCacheRoute / @DeleteCacheFn clears the intended keys

Common mistakes

  • Putting @Cache on a service or controller instead of the root @Module
  • Omitting name when the plugin declares more than one @Cache
  • Hard-coding Redis credentials instead of env
  • Re-declaring @Cache in consumers instead of @CacheClient
  • Mismatching plugin + name between @CacheClient and @CacheRoute / @CacheFn
  • Forgetting pluginDependencies so the owner loads after the consumer