@abinashpatri/cache
v1.0.1
Published
Production-grade cache & rate limit utility library
Readme
@abinashpatri/cache
Production-ready Redis cache helper for Node.js and TypeScript.
Built for simple integration with:
- safe JSON serialization/deserialization
- TTL with jitter to reduce cache stampedes
- auto-cache wrapper (
remember) - Redis-backed Express rate limiting (
express-rate-limit+ Redis store) - ESM + CommonJS support
- full TypeScript typings
Installation
npm install @abinashpatri/cacheRequirements
- Node.js 18+
- A running Redis instance
Default Redis URL used by the library:
redis://127.0.0.1:6379Quick Start
import { connect, remember, disconnect } from "@abinashpatri/cache";
async function main() {
await connect(process.env.REDIS_URL);
const user = await remember("user:42", 60, async () => {
// Replace with DB/API call
return { id: 42, name: "Abinash" };
});
console.log(user);
await disconnect();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});API
connect(url?: string): Promise<void>
Initializes Redis connection safely.
- idempotent (safe to call multiple times)
- default URL is
redis://127.0.0.1:6379
await connect();
// or
await connect("redis://localhost:6379");disconnect(): Promise<void>
Gracefully closes Redis connection.
await disconnect();ping(): Promise<string>
Health check for Redis connection.
const status = await ping(); // "PONG"get<T = unknown>(key: string): Promise<T | null>
Reads and parses JSON value from cache.
- returns
nullwhen key does not exist - returns
nullwhen cached value is invalid JSON
const profile = await get<{ id: number; name: string }>("user:42");set<T>(key: string, value: T, ttl = 60): Promise<void>
Stores JSON value in cache with TTL (seconds).
- adds jitter (
0-9s) to TTL internally to spread expirations
await set("user:42", { id: 42, name: "Abinash" }, 120);del(key: string): Promise<void>
Deletes a cache key.
await del("user:42");remember<T>(key: string, ttl: number, fn: () => Promise<T>): Promise<T>
Auto-cache helper:
- returns cached value when available
- otherwise executes
fn - stores result in Redis with TTL
- returns computed result
const posts = await remember("posts:home", 30, async () => {
return fetchPostsFromDb();
});createRedisRateLimiter(options?): RateLimitRequestHandler
Creates an Express middleware that stores rate-limit counters in Redis.
Defaults:
windowMs:15 * 60 * 1000(15 minutes)limit:100standardHeaders:"draft-7"legacyHeaders:false- Redis key
prefix:"rl:"
import express from "express";
import { connect, createRedisRateLimiter } from "@abinashpatri/cache";
const app = express();
await connect(process.env.REDIS_URL);
const limiter = createRedisRateLimiter({
windowMs: 15 * 60 * 1000,
limit: 100,
prefix: "rate-limit:",
message: {
success: false,
message: "Too many requests, please try again later.",
},
});
app.use("/api", limiter);
app.get("/api/health", (_req, res) => {
res.json({ ok: true });
});Important: call connect() before creating or using the limiter so Redis is ready.
Rate Limit Patterns (Express)
1) Global limiter (all routes)
const globalLimiter = createRedisRateLimiter({
windowMs: 15 * 60 * 1000,
limit: 300,
prefix: "rl:global:",
});
app.use(globalLimiter);2) Router-level limiter (only /api)
const apiLimiter = createRedisRateLimiter({
windowMs: 60 * 1000,
limit: 100,
prefix: "rl:api:",
});
app.use("/api", apiLimiter);3) Route-level limiter (single endpoint)
const loginLimiter = createRedisRateLimiter({
windowMs: 10 * 60 * 1000,
limit: 5,
prefix: "rl:login:",
});
app.post("/auth/login", loginLimiter, loginHandler);4) Different limits for different routes
const strictLimiter = createRedisRateLimiter({ windowMs: 60 * 1000, limit: 20, prefix: "rl:strict:" });
const relaxedLimiter = createRedisRateLimiter({ windowMs: 60 * 1000, limit: 200, prefix: "rl:relaxed:" });
app.use("/auth", strictLimiter);
app.use("/public", relaxedLimiter);5) Per-user/API key rate limit (custom key generator)
const userLimiter = createRedisRateLimiter({
windowMs: 60 * 1000,
limit: 60,
prefix: "rl:user:",
keyGenerator: (req) => {
const userId = req.headers["x-user-id"];
if (typeof userId === "string" && userId.length > 0) return `user:${userId}`;
return req.ip ?? "unknown";
},
});
app.use("/v1", userLimiter);6) Skip successful requests (limit only failed attempts)
Useful for brute-force protection on auth endpoints.
const authLimiter = createRedisRateLimiter({
windowMs: 15 * 60 * 1000,
limit: 10,
prefix: "rl:auth:",
skipSuccessfulRequests: true,
});
app.post("/auth/login", authLimiter, loginHandler);7) Custom 429 response handler
const customHandlerLimiter = createRedisRateLimiter({
windowMs: 60 * 1000,
limit: 50,
prefix: "rl:custom:",
handler: (req, res) => {
res.status(429).json({
success: false,
message: "Rate limit exceeded",
route: req.originalUrl,
});
},
});
app.use("/api", customHandlerLimiter);8) Proxy/LB deployment setup (important)
app.set("trust proxy", 1);Set this when running behind Nginx, Cloudflare, Render, Railway, Heroku, or similar proxy/CDN, so client IP based limiting is correct.
Import Styles
Named imports (recommended)
import {
connect,
get,
set,
del,
remember,
ping,
disconnect,
createRedisRateLimiter,
} from "@abinashpatri/cache";Default import
import cache from "@abinashpatri/cache";
await cache.connect();
await cache.set("k", { ok: true }, 60);
const limiter = cache.createRedisRateLimiter({ limit: 50 });Production Usage Notes
- Call
connect()once during app startup. - Reuse this single connection across your application.
- Always
await disconnect()during graceful shutdown. - Keep TTL short for rapidly changing data and longer for stable data.
- Use key namespacing (example:
service:entity:id) to avoid collisions.
Error Handling Pattern
import { connect } from "@abinashpatri/cache";
try {
await connect(process.env.REDIS_URL);
} catch (err) {
console.error("Failed to connect Redis", err);
// decide fallback strategy (disable cache / fail fast)
}License
MIT
