Imported from AbelMaireg/backend-boilerplate-nestjs (
libs/cache/AGENTS.md). Install upstream withnpx skills add AbelMaireg/backend-boilerplate-nestjs --skill cache. Copyright stays with the author.
libs/cache — Usage Guide
How to use the cache module from feature code. Import path: cache/cache.
Getting started
Inject CacheService anywhere (the module is @Global, so no need to import CacheModule):
import { CacheService } from 'cache/cache';
@Injectable()
export class ReportService {
constructor(private readonly cache: CacheService) {}
}
Bare methods act on the default store (set by CACHE_DRIVER). Use store(name) to target
another configured store.
Reading
await this.cache.get<User>('user:1'); // value, or undefined on miss
await this.cache.get('user:1', fallbackUser); // default value on miss
await this.cache.get('user:1', () => loadUser()); // default closure (only called on miss)
await this.cache.many<User>(['user:1', 'user:2']); // { 'user:1': ..., 'user:2': undefined }
await this.cache.has('user:1'); // boolean
await this.cache.missing('user:1'); // boolean
await this.cache.ttl('user:1'); // remaining ms, or null if absent/no expiry
Writing
await this.cache.put('user:1', user, 60); // TTL in seconds
await this.cache.put('user:1', user, new Date(Date.now() + 60_000)); // absolute expiry
await this.cache.put('user:1', user); // no TTL → module default (or forever)
await this.cache.forever('config', cfg); // never expires
await this.cache.add('lock:once', 1, 60); // write ONLY if absent → boolean
await this.cache.putMany({ a: 1, b: 2 }, 60); // batch write, one shared TTL
Compute-and-store (remember)
Return the cached value, or run the callback once, store its result, and return it:
const stats = await this.cache.remember('stats', 300, () =>
this.computeStats(),
);
const menu = await this.cache.rememberForever('menu', () => this.buildMenu());
Counters
await this.cache.increment('hits'); // +1 → new value
await this.cache.increment('hits', 5); // +5
await this.cache.decrement('hits', 2); // -2
Retrieve-and-remove / removal
await this.cache.pull<Token>('one-time'); // return then delete
await this.cache.forget('user:1'); // delete one key
await this.cache.flush(); // clear the whole store
Choosing a store
await this.cache.get('k'); // default store
await this.cache.store('memory').put('k', v, 10); // in-process store, explicitly
await this.cache.store('redis').put('k', v, 60); // redis store
memory is always available. redis is available when CACHE_DRIVER=redis, or when another
module requires it regardless of the app's default driver (e.g. libs/rate-limit with
RATE_LIMIT_DRIVER=redis); otherwise calling store('redis') throws.
Atomic locks
Run a critical section once at a time. Prefer the callback form — the lock auto-releases even if the callback throws:
// Acquire, run, auto-release. Returns the callback result, or false if not acquired.
const ran = await this.cache.lock('job:42', 30).get(() => this.runJob(42));
if (ran === false) return; // someone else holds it
// Wait up to 5s for the lock; throws LockTimeoutException on timeout.
await this.cache.lock('report', 30).block(5, () => this.generate());
Manual control:
const lock = this.cache.lock('job:42', 30);
if (await lock.acquire()) {
try {
await this.runJob(42);
} finally {
await lock.release();
}
}
Across processes: pass lock.owner() to another worker and release it there with
this.cache.restoreLock('job:42', owner).release() (or .forceRelease() to break a stuck lock).
Locks work across instances only on the redis store; the memory store is single-process.
Tags
Group related entries and invalidate them all at once:
await this.cache.tags(['users']).put('user:1', user, 600);
await this.cache.tags(['users']).put('user:2', user2, 600);
await this.cache.tags(['users']).get('user:1'); // read with the SAME tags
await this.cache.tags(['users']).flush(); // drop everything tagged 'users'
Rules to remember:
- Read with the same tag set you wrote with.
- Tagged and untagged entries are separate:
cache.get('user:1')will not seecache.tags(['users']).put('user:1', ...). - Always give tagged writes a TTL — flushed entries are orphaned, not deleted.
TaggedCache supports get, has, put, add, forever, forget, pull, remember,
rememberForever, and flush.
TTL cheatsheet
number→ seconds from now ·Date→ absolute expiry · omitted /null→ forever (or the module default TTL if configured).putManyand tagged writes apply one TTL to the whole batch.increment/decrementonredisdo not set or change a TTL —putthe key with a TTL first if the counter should expire. (Thememorystore keeps the existing entry's TTL.)- Treat values as plain JSON data — redis serialises to JSON, so class instances/functions won't round-trip with their prototype.
Configuration
Set via env (see .env.example); the module reads them through libs/config.
| Env var | Default | Purpose |
|---|---|---|
CACHE_DRIVER |
memory |
Default store: memory or redis |
CACHE_PREFIX |
boilerplate_cache |
Key prefix for every entry |
CACHE_DEFAULT_TTL |
(forever) | Default TTL (seconds) for put/add/remember |
CACHE_LOG_HITS |
false |
Record hit/miss/write on the request wide event (via LoggerService) |
CACHE_MEMORY_MAX_ITEMS |
0 |
Memory store entry cap (0 = unbounded) |
CACHE_REDIS_URL |
(none) | Redis connection string (overrides host/port/db) |
CACHE_REDIS_HOST / CACHE_REDIS_PORT / CACHE_REDIS_PASSWORD / CACHE_REDIS_DB |
localhost / 6379 / — / 0 |
Redis connection parts |
Common recipes
// Cache an expensive per-user read for 5 minutes
const dash = await this.cache.remember(`dash:${userId}`, 300, () =>
this.dashboards.build(userId),
);
// Invalidate everything for a user on change
await this.cache.tags([`user:${userId}`]).flush();
// Run a job once across instances (redis default)
const ran = await this.cache.lock(`job:${id}`, 30).get(() => this.runJob(id));
if (ran === false) return;
// First-hit window for a counter
const n = await this.cache.increment(`hits:${ip}`);
if (n === 1) await this.cache.put(`hits:${ip}`, 1, 60);