Skip to content

Repository files navigation

cache-viewer

Embeddable web UI for inspecting in-process memory caches (and Redis) in Node.js services. One-line integration, zero required dependencies beyond your framework.

app.use("/cache-ui", cacheViewer({ adapter: fromCacheManager(cache) }));

Browse keys, inspect values (pretty-printed JSON with a live TTL countdown), and delete entries — all from a single self-contained HTML page with no build step and no third-party scripts.

Searchable key list beside a value inspector showing a pretty-printed JSON entry, its TTL badge, and a delete button

Inspecting a cache-manager@7 (Keyv) store. The header names the backend and the host — which matters when the cache is per-pod.

⚠️ Security warning

Cache values often contain PII, session data, tokens, or other secrets. This viewer exposes them, unredacted, to anyone who can reach the mounted route. Treat it accordingly:

  • It is disabled by default in production (NODE_ENV === "production") — you must opt in explicitly if you want it there.
  • Gate it behind your own authentication (see Auth below) before enabling it anywhere reachable outside a trusted network.
  • Never mount it on a public-facing router without an auth guard.
  • When disabled or when auth rejects a request, every route (UI and API) responds 404, not 403 — an unauthenticated caller can't even tell the viewer exists.

Install

npm install cache-viewer

express is an optional peer dependency, required only if you use the cache-viewer/express subpath.

Integration

cache-manager v5

import { caching } from "cache-manager";
import { fromCacheManager } from "cache-viewer";
import { cacheViewer } from "cache-viewer/express";

const cache = await caching("memory", { max: 1000, ttl: 60_000 });

app.use("/cache-ui", cacheViewer({ adapter: fromCacheManager(cache) }));

cache-manager v6/v7 (Keyv-backed)

import { createCache } from "cache-manager";
import Keyv from "keyv";
import { fromKeyv } from "cache-viewer";
import { cacheViewer } from "cache-viewer/express";

const keyv = new Keyv();
const cache = createCache({ stores: [keyv] });

app.use("/cache-ui", cacheViewer({ adapter: fromKeyv(keyv) }));

Not every Keyv store implements iterator(). When it doesn't, keys() returns null and the UI shows a "keys not enumerable" empty state instead of an empty list — the two are deliberately distinguished.

Redis (ioredis)

import Redis from "ioredis";
import { fromRedis } from "cache-viewer";
import { cacheViewer } from "cache-viewer/express";

const redis = new Redis(process.env.REDIS_URL);

app.use("/cache-ui", cacheViewer({ adapter: fromRedis(redis) }));

Key enumeration uses SCAN (never KEYS), so it's safe against a production-sized Redis instance.

Auth

app.use(
  "/cache-ui",
  cacheViewer({
    adapter: fromCacheManager(cache),
    enabled: true, // override the NODE_ENV default if you must run this in prod
    auth: (req) => req.headers["x-admin-token"] === process.env.CACHE_VIEWER_TOKEN,
  }),
);
  • enabled defaults to process.env.NODE_ENV !== "production".
  • auth(req) receives the Express request; return false to hide the viewer as if it were disabled (404, not 403).
  • Prefer wiring the viewer behind whatever session/SSO middleware already gates the rest of your admin surface, rather than inventing a bespoke token scheme.

Kubernetes / per-pod caches

An in-process memory cache (fromCacheManager, most Keyv memory stores) exists per pod, not cluster-wide. If you're running multiple replicas, each pod has its own independent cache, and hitting /cache-ui through a load balancer will land you on a different pod's data every request.

The header always shows hostname (os.hostname(), i.e. the pod name) next to the backend label so you can tell which replica you're actually looking at. To inspect a specific pod deterministically:

  • kubectl port-forward pod/<pod-name> 3000:3000 and hit that pod directly, or
  • Set a sticky-session / pod-affinity rule on the ingress in front of the route while debugging.

A Redis-backed cache (fromRedis) doesn't have this problem — it's already a single shared backend regardless of which pod serves the request.

API (framework-agnostic)

If you're not using Express, wire the JSON API into your own server with createApiHandler from the package root, and serve ui/index.html (shipped in the published package) yourself:

import { createApiHandler, fromCacheManager } from "cache-viewer";

const handleCacheApi = createApiHandler({ adapter: fromCacheManager(cache) });

server.on("request", async (req, res) => {
  const handled = await handleCacheApi(req, res);
  if (!handled) {
    // fall through to the rest of your routing
  }
});

Routes, relative to wherever you mount it:

Method Path Response
GET /api/keys?q= { keys: string[] | null }
GET /api/keys/:key { value, ttlMs, serialized }
DELETE /api/keys/:key 204
GET /api/stats { count, backend, hostname }

Manual check

After building (npm run build), mount the viewer in a throwaway Express app and open it in a browser:

// scratch.mjs — a hand-rolled adapter is enough to exercise the UI without pulling in a
// specific cache-manager major.
import express from "express";
import { cacheViewer } from "./dist/express.js";

const store = new Map([["demo:1", { hello: "world" }]]);
const expiresAt = Date.now() + 30_000;

const adapter = {
  async keys(pattern) {
    const all = [...store.keys()];
    return pattern ? all.filter((k) => k.includes(pattern)) : all;
  },
  async get(key) {
    return store.has(key) ? { value: store.get(key), ttlMs: expiresAt - Date.now() } : null;
  },
  async del(key) {
    store.delete(key);
  },
  async stats() {
    return { count: store.size, backend: "demo", hostname: "localhost" };
  },
};

const app = express();
app.use("/cache-ui", cacheViewer({ adapter }));
app.listen(3000, () => console.log("open http://localhost:3000/cache-ui/"));
node scratch.mjs

Then open http://localhost:3000/cache-ui/ and confirm: the key list shows demo:1, the TTL countdown ticks down, search filters the list, and deleting the key removes it after confirmation.

Out of scope for v0.1

  • RESP protocol shim (connecting RedisInsight directly)
  • WebSocket live updates (2s polling is used instead)
  • NestJS binding
  • Editing/setting values — this is a viewer, not an editor

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages