npm package: @heinhtet37/express-route-cache
Description: Reusable Express route caching middleware for Node.js and TypeScript services.
Latest version: 0.2.0

# @heinhtet37/express-route-cache

Express route caching helpers for low-level control and practical resource-level DX.

The original API still works:

- `createRouteCache`
- `createRedisStore`
- `createMemoryStore`
- `check`
- `capture`
- `invalidate`

This version adds:

- `resource()` bundles for list/detail/variant routes
- optional cache key `namespace`
- optional tag-based invalidation
- deterministic resource keys
- lightweight TTL parsing like `"5m"` and `"1h"`
- lightweight lifecycle events

## Install

```bash
npm install @heinhtet37/express-route-cache
```

## Quick Start

```ts
import { createRedisStore, createRouteCache } from "@heinhtet37/express-route-cache";
import redis from "./redis";

const cache = createRouteCache({
  store: createRedisStore({
    client: redis,
    defaultTtlSeconds: "5m",
  }),
  namespace: "mm-smart-pos",
});

const rolesCache = cache.resource({
  basePath: "/api/v1/roles",
  idParam: "id",
  ttlSeconds: "5m",
  variants: ["names"],
});

router.get("/", ...rolesCache.list(), async (_req, res) => {
  res.json(await roleService.getAll());
});

router.get("/names", ...rolesCache.variant("names"), async (_req, res) => {
  res.json(await roleService.getAllNames());
});

router.get("/:id", ...rolesCache.detail(), async (req, res) => {
  res.json(await roleService.getById(req.params.id));
});

router.post("/", rolesCache.invalidateOnWrite(), async (req, res) => {
  res.json(await roleService.create(req.body));
});

router.patch("/:id", rolesCache.invalidateOnWrite(), async (req, res) => {
  res.json(await roleService.update(req.params.id, req.body));
});

router.delete("/:id", rolesCache.invalidateOnWrite(), async (req, res) => {
  res.json(await roleService.remove(req.params.id));
});
```

## Resource API

`resource()` is the high-level DX API for common REST-style routes.

```ts
const usersCache = cache.resource({
  basePath: "/api/users",
  idParam: "id",
  ttlSeconds: 300,
  variants: ["names", "summary"],
});
```

### Read Bundles

`list()` returns `[check, capture]` for the collection route.

```ts
router.get("/", ...usersCache.list(), handler);
```

`detail()` returns `[check, capture]` for `/:id`.

```ts
router.get("/:id", ...usersCache.detail(), handler);
```

`variant(name)` returns `[check, capture]` for custom read routes like `/names`.

```ts
router.get("/names", ...usersCache.variant("names"), handler);
```

### Write Invalidation

`invalidateOnWrite()` invalidates the resource collection, query variants, configured variants, and the matching detail route when the write route has an `id` param.

```ts
router.post("/", usersCache.invalidateOnWrite(), handler);
router.patch("/:id", usersCache.invalidateOnWrite(), handler);
router.delete("/:id", usersCache.invalidateOnWrite(), handler);
```

You can extend invalidation with extra patterns or tags:

```ts
router.patch(
  "/:id",
  usersCache.invalidateOnWrite({
    additionalPatterns: ["/api/dashboard*", "/api/summary*"],
    additionalTags: ["dashboard"],
  }),
  handler
);
```

## Low-Level API

The original low-level API remains available for custom routes.

```ts
const cache = createRouteCache({
  store: createRedisStore({
    client: redis,
    defaultTtlSeconds: 300,
  }),
});

const checkUsersCache = cache.check({
  varyHeaders: ["accept-language"],
});

const captureUsersCache = cache.capture({
  ttlSeconds: "5m",
});

const invalidateUsersCache = cache.invalidate({
  patterns: (req) => [
    "/api/users",
    "/api/users?*",
    `/api/users/${req.params.id}*`,
  ],
});
```

## Namespace Support

`namespace` prefixes internal cache keys and tag identifiers without forcing consumers to rewrite their patterns.

```ts
const cache = createRouteCache({
  store,
  namespace: "mm-smart-pos",
});

await cache.set("/roles", { ok: true }, 300);
await cache.deleteByPatterns(["/roles"]);
```

With a namespace configured, `"/roles"` becomes an internal key like `"mm-smart-pos::/roles"`, but callers can still use the simple public key.

## Tag Support

Tag invalidation is optional.

- `createMemoryStore()` supports tags out of the box.
- `createRedisStore()` supports tags when the Redis client provides `sadd`, `srem`, `smembers`, and `expire`.
- Pattern invalidation continues to work either way.

Low-level tag example:

```ts
const checkUsersCache = cache.check({ key: "/api/users" });

const captureUsersCache = cache.capture({
  tags: ["users", "admin-dashboard"],
});

const invalidateUsersByTag = cache.invalidate({
  tags: ["users"],
});
```

Resource bundles automatically attach resource-level tags, and `invalidateOnWrite()` uses both patterns and tags when available.

## TTL Values

TTL values can be numbers or simple strings:

- `300`
- `"300"`
- `"5m"`
- `"1h"`
- `"1d"`

This works with:

- `defaultTtlSeconds`
- `capture({ ttlSeconds })`
- `resource({ ttlSeconds })`
- `set(key, value, ttl)`

## Events

You can subscribe to lightweight lifecycle events:

- `hit`
- `miss`
- `set`
- `invalidate`
- `error`

```ts
const cache = createRouteCache({
  store,
  events: {
    hit: ({ key }) => console.log("cache hit", key),
    miss: ({ key }) => console.log("cache miss", key),
    set: ({ key, ttlSeconds }) => console.log("cache set", key, ttlSeconds),
    invalidate: ({ patterns, tags }) => console.log("cache invalidate", patterns, tags),
    error: ({ operation, error }) => console.error("cache error", operation, error),
  },
});
```

## Redis Store

```ts
import Redis from "ioredis";
import { createRedisStore } from "@heinhtet37/express-route-cache";

const client = new Redis();

const store = createRedisStore({
  client,
  defaultTtlSeconds: "5m",
});
```

For pattern invalidation, the Redis client must provide:

- `get`
- `del`
- one of `scan` or `keys`
- one of `setex` or `set(..., "EX", ttl)`

For tag invalidation, the Redis client should also provide:

- `sadd`
- `srem`
- `smembers`
- `expire`

## Memory Store

```ts
import { createMemoryStore } from "@heinhtet37/express-route-cache";

const store = createMemoryStore({
  defaultTtlSeconds: "1m",
});
```

The memory store is useful for local development and tests. It also supports tag invalidation.

## Notes And Tradeoffs

- The old API is still supported and unchanged for normal usage.
- The new `resource()` API is intentionally opinionated around list/detail/variant patterns.
- Resource invalidation automatically covers configured variants only. If you use extra variant names, either include them in `variants` or add `additionalPatterns`.
- Tag invalidation is additive. It does not replace pattern invalidation.
- Redis tag invalidation uses extra metadata keys to track tag membership. This keeps the public API simple at the cost of slightly more write work per cached entry.

## Tests

```bash
npm test
```

That runs a package build and then the Node test suite in `tests/`.