@heinhtet37/express-route-cache
v0.2.0
Published
Reusable Express route caching middleware for Node.js and TypeScript services.
Maintainers
Readme
@heinhtet37/express-route-cache
Express route caching helpers for low-level control and practical resource-level DX.
The original API still works:
createRouteCachecreateRedisStorecreateMemoryStorecheckcaptureinvalidate
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
npm install @heinhtet37/express-route-cacheQuick Start
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.
const usersCache = cache.resource({
basePath: "/api/users",
idParam: "id",
ttlSeconds: 300,
variants: ["names", "summary"],
});Read Bundles
list() returns [check, capture] for the collection route.
router.get("/", ...usersCache.list(), handler);detail() returns [check, capture] for /:id.
router.get("/:id", ...usersCache.detail(), handler);variant(name) returns [check, capture] for custom read routes like /names.
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.
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:
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.
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.
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 providessadd,srem,smembers, andexpire.- Pattern invalidation continues to work either way.
Low-level tag example:
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:
defaultTtlSecondscapture({ ttlSeconds })resource({ ttlSeconds })set(key, value, ttl)
Events
You can subscribe to lightweight lifecycle events:
hitmisssetinvalidateerror
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
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:
getdel- one of
scanorkeys - one of
setexorset(..., "EX", ttl)
For tag invalidation, the Redis client should also provide:
saddsremsmembersexpire
Memory Store
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
variantsor addadditionalPatterns. - 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
npm testThat runs a package build and then the Node test suite in tests/.
