The canonical reference application for @bymax-one/nest-cache —
a typed Redis cache module for NestJS, exercised end to end across a NestJS API and a Next.js dashboard that makes
the invisible parts of caching (namespace isolation, TTL expiry, Pub/Sub fan-out, single-flight) tangible on screen.
📦 Library · 🚀 Quick Start · ✅ Features · 🏗️ Architecture · 🔌 Topologies · 📖 Docs
@bymax-one/nest-cache is the what; this repository is the how. It is a runnable, production-shaped demo
that exercises every public export of the library across a NestJS API and a first-class Next.js observability
dashboard — read-through caching, namespace/tenant isolation, custom serialization, Pub/Sub, TTL keyspace events,
Lua single-flight, the full connection-topology matrix, and the Cache Admin (Explorer) backend — all against a real
Redis 7. The demo domain is an in-memory multi-tenant product catalogue, chosen because it naturally exercises
strings, numerics, hashes, sets, batch pipelines, SCAN, and TTL.
git clone https://github.com/bymaxone/nest-cache-example.git
cd nest-cache-example
pnpm install
pnpm infra:up # Redis 7 on 127.0.0.1:6379 (add --profile tools for RedisInsight :5540)
pnpm dev # api → http://localhost:3001 · web → http://localhost:3000The library is pre-publish — it is consumed via a local
file:link to the sibling../nest-cachecheckout until it ships to npm. Build that checkout first (cd ../nest-cache && pnpm install && pnpm build).
No
.envis required — every variable is defaulted for local standalone Redis. Override viaapps/api/.env(seeapps/api/.env.example);CACHE_MODEswitches the topology andCACHE_SERIALIZER=msgpackswaps in the binary codec.
Read-through & data structures
- ✅ Read-through catalog (
get/set), batchmget/mset,setNxseed, and TTL ops (expire/persist/ttl) - ✅ Atomic numerics (
incr/decr), hashes (carts), and sets (tags) — every structure the typed facade exposes
Namespaces & multi-tenancy
- ✅ One
namespaceper instance; tenants modeled as prefix scoping (cache-example:tenant:{id}:…) - ✅
flushNamespace()(SCAN + UNLINK) with a live isolation proof — a foreign-namespace key survives the flush
Serialization
- ✅ Default
JsonSerializer+ a custom MessagePackISerializer(opt-in viaCACHE_SERIALIZER=msgpack) - ✅
getRaw/setRawcodec-bypass contrast; the active codec injected via theBYMAX_CACHE_SERIALIZERtoken
Pub/Sub & real-time
- ✅ Exact + pattern subscribe (
subscribe/psubscribe), ref-counted unsubscribe, handler-error isolation - ✅ A socket.io bridge fans Redis events out to every browser tab over 3 multiplexed channels (receive-only)
TTL keyspace events
- ✅ The library's documented escape hatch — a dedicated raw subscriber on
__keyevent@<db>__:expired, namespace-filtered - ✅ A live countdown wall: seed a short-TTL key, watch the ring drain and the
cache:expiredevent arrive
Lua & cache stampede
- ✅ Single-flight collapse: N concurrent contenders → 1 origin fetch + (N−1) cache hits via a
SET NX PXLua lock - ✅ Token-safe release (compare-and-delete) and the stable script SHA1 resolved through
ScriptManagerService
Connection & error surface
- ✅ Standalone · Sentinel · Cluster topologies (Docker Compose profiles) with an ioredis
natMapfor NAT'd stacks - ✅ All 15
CacheExceptioncodes mapped to their canonical HTTP status by a single global exception filter
Cache Admin / Explorer backend
- ✅
scan(non-blocking cursor) vskeys(O(N), guarded), pipeline bulk seed, parsed RedisINFO, keyspace breakdown - ✅ Per-key inspect / delete / persist / expire — the backend behind the dashboard's Key Explorer
The dashboard (apps/web)
- ✅ 10 pages — Overview · Explorer · Playground · Tenants · Pub/Sub · TTL Live · Stampede · Serializer · Errors · Connection
- ✅ Recharts golden-signal panels, custom-SVG TTL rings + stampede swimlane,
nuqsURL state, library-clean bundle
Quality bar
- ✅ 100% unit coverage (api Jest + web Vitest) · E2E of every HTTP + WebSocket flow (Testcontainers)
- ✅ Stryker mutation — api 100% (0 survivors) · web 91.61% (
lib/**100%) · 18 Playwright journeys - ✅ No Swagger (JSDoc + Zod DTOs) · English-only · Conventional Commits · zero suppression comments
apps/web (Next.js 16 · React 19 · Tailwind 4)
10 dashboard pages — Observe · Real-time · Labs · System
imports @bymax-one/nest-cache/shared (zero-dep) → typed error codes + connection status
│ REST (fetch → ApiResult<T>) ▲ socket.io (3 channels: connection · event · expired)
▼ │
┌─────────────────────────────────────────────────┴────────────────┐
│ apps/api (NestJS 11) │
│ BymaxCacheModule.forRootAsync → buildCacheOptions(env) │
│ read-through · namespaces/tenants · serialization · Pub/Sub · │
│ TTL events · Lua single-flight · Cache Admin · exception filter │
└───────────────────────────────┬──────────────────────────────────┘
│ ioredis 5
▼
┌─────────────────────┐
│ Redis 7 │ standalone (default)
│ 127.0.0.1:6379 │ · sentinel · cluster (compose profiles)
└─────────────────────┘
RedisInsight :5540 (docker compose --profile tools)
apps/api and apps/web are independently deployable. The web bundle imports only the zero-dependency
@bymax-one/nest-cache/shared subpath (proving @nestjs/ioredis never leak into the browser) and bridges live
Redis events to the dashboard over socket.io. Full diagram and API contracts in
docs/TECHNICAL_SPECIFICATION.md.
Coverage rule. Every public export of
@bymax-one/nest-cache(the.and/sharedsubpaths) is referenced from at least one file underapps/— the spec's Feature Coverage Matrix maps each one to where it is used, and an export-usage audit enforces it.
The API runs standalone by default and can run against sentinel or cluster via Docker Compose profiles.
CACHE_MODE selects which connection block cache.config.ts builds. Sentinel and cluster report internal
addresses, so a host process reaches them through an ioredis natMap that rewrites those to the published
127.0.0.1 ports — the natMap env is opt-in (standalone and production never set it).
# Standalone (default) — just Redis on 127.0.0.1:6379
pnpm infra:up
# Sentinel — 1 master + 2 replicas + 3 sentinels
docker compose --profile sentinel up -d --wait
CACHE_MODE=sentinel REDIS_SENTINELS=127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381 \
REDIS_SENTINEL_MASTER=mymaster REDIS_SENTINEL_NAT_MAP="redis-master:6379=127.0.0.1:6380" \
pnpm --filter api start
# Cluster — 3 cluster-enabled primaries (auto-formed by the cluster-init service)
docker compose --profile cluster up -d --wait
CACHE_MODE=cluster REDIS_CLUSTER_NODES=127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002 \
REDIS_CLUSTER_NAT_MAP="172.31.0.11:7000=127.0.0.1:7000,172.31.0.12:7001=127.0.0.1:7001,172.31.0.13:7002=127.0.0.1:7002" \
pnpm --filter api startIn cluster mode scan, flushNamespace, and getClient raise cache.unsupported_in_cluster — the admin
endpoints that use them surface it cleanly through the exception filter; eval requires ≥1 key (routed by slot) and
Pub/Sub is an experimental passthrough. See TECHNICAL_SPECIFICATION.md §15.
pnpm test # unit — api (Jest) + web (Vitest), 100% coverage gate
pnpm test:e2e # E2E — api flows (Testcontainers redis:7) + web journeys (Playwright, self-booting stack)
pnpm mutation # Stryker — api break:100 · web break:90 (serialized; reports under reports/mutation/)
pnpm lint && pnpm typecheck && pnpm format:checkThe web E2E auto-boots its own stack (a dedicated test Redis via docker-compose.test.yml + the API + the
dashboard) through Playwright's webServer, so pnpm test:e2e:web is self-contained. Mutation results by feature
group are in docs/stryker/HISTORY.md.
| Doc | What it covers |
|---|---|
| TECHNICAL_SPECIFICATION | Architecture, API contracts & the feature-coverage matrix (master spec) |
| DASHBOARD | The apps/web observability console — page inventory & design spec |
| DEVELOPMENT_PLAN | The phased build plan & quality gates (Appendix C) |
| stryker/HISTORY | Mutation run history + final scores by feature group |
| stryker/IMPLEMENTATION_PLAN | Mutation hardening order, stack gotchas & equivalent-mutants table |
MIT © Bymax One. @bymax-one/nest-cache is MIT © Bymax One. See LICENSE.