universal-api-cache
v1.2.1
Published
Framework-agnostic caching middleware for Node.js with in-memory (L1) and Redis (L2)
Downloads
8
Maintainers
Readme
universal-api-cache
Framework-agnostic caching middleware for Node.js with L1 (memory) and L2 (Redis) support.
Features:
- Cache GET by default, optional POST (idempotent) with
cachePostPredicate - Pattern-based cache invalidation with auto-generated REST rules
- Smart invalidation for collections, items, and nested resources
- Custom invalidation patterns with placeholder support (
{userId},{postId}, etc.) - User scoping for multi-tenant applications
- Stale-While-Revalidate
- Automatic invalidation on write (POST/PUT/PATCH/DELETE)
- Request coalescing
- Per-route TTL
- Skip via
skipCachePredicate - Multi-store (memory + Redis)
Install
npm install universal-api-cacheUsage (Express)
import express from 'express';
import { apiCache } from 'universal-api-cache';
const app = express();
app.use(express.json());
app.use(
apiCache({
ttl: 60,
methods: ['GET', 'POST'],
useMemory: true,
useRedis: false,
redisUrl: 'redis://localhost:6379',
invalidateOnWrite: true,
staleWhileRevalidate: true,
excludePaths: ['/auth/login'],
skipCachePredicate: (req) => !!req.headers['x-no-cache'],
cachePostPredicate: (req) => req.path === '/search',
getUserId: (req) => req.user?.id,
// Pattern-based invalidation configuration
invalidation: {
autoInvalidateRules: {
autoGenerateRestRules: true,
autoInvalidateParents: true,
autoInvalidateCollections: true
},
respectUserScope: true,
enableInvalidationDebugging: true
}
}),
);
app.get('/users', (req, res) => res.json([{ id: 1, name: 'Alice' }]))
app.post('/search', (req, res) => res.json({ query: req.body.query, results: ['item1'] }))API
apiCache(options)returns an Express-compatible middleware function with methods:clearCache(pattern?: string)to invalidate keys by patterngetCacheStats()to get{ hits, misses, keys }
Configuration Options
interface ApiCacheOptions {
// Basic caching
ttl?: number; // Cache TTL in seconds (default: 60)
methods?: string[]; // HTTP methods to cache (default: ['GET'])
useMemory?: boolean; // Enable memory store (default: true)
useRedis?: boolean; // Enable Redis store (default: false)
redisUrl?: string; // Redis connection URL
// Cache behavior
invalidateOnWrite?: boolean; // Auto-invalidate on POST/PUT/PATCH/DELETE
staleWhileRevalidate?: boolean; // Serve stale data while refreshing
maxPayloadSize?: number; // Max response size to cache
// Request filtering
excludePaths?: string[]; // Paths to never cache
skipCachePredicate?: (req) => boolean; // Custom skip logic
cachePostPredicate?: (req) => boolean; // When to cache POST requests
disableAuthCaching?: boolean; // Skip caching for authenticated users
// User identification
getUserId?: (req) => string; // Extract user ID for scoping
getPerRouteTtl?: (req) => number; // Dynamic TTL per route
// Pattern-based invalidation
invalidation?: InvalidationOptions;
// Manual invalidation
getInvalidationPatterns?: (req) => string[]; // Custom invalidation logic
// Logging
logger?: {
debug?: (message: string, ...args: any[]) => void;
info?: (message: string, ...args: any[]) => void;
warn?: (message: string, ...args: any[]) => void;
};
}InvalidationOptions (Pattern-Based Cache Invalidation)
interface InvalidationOptions {
// Enable automatic pattern-based invalidation
enablePatternInvalidation?: boolean; // Enable the entire invalidation system (default: true when invalidation config exists)
// Built-in invalidation rules
autoInvalidateRules?: {
// Automatically create rules for common REST patterns
autoGenerateRestRules?: boolean; // Auto-create REST invalidation patterns (default: false)
// Auto-invalidate parent resources when child resources change
autoInvalidateParents?: boolean; // POST /api/users/123/posts → invalidates GET /api/users/123* (default: true)
// Auto-invalidate collection when items change
autoInvalidateCollections?: boolean; // POST /api/users → invalidates GET /api/users* (default: true)
};
// Custom invalidation rules (advanced)
invalidationRules?: InvalidationRule[]; // Array of custom invalidation rules
// User scoping and security
respectUserScope?: boolean; // Only invalidate cache for the current user (default: true)
// Performance and safety limits
maxInvalidationDepth?: number; // Prevent infinite recursion in rule chains (default: 3)
invalidationTimeout?: number; // Timeout for invalidation operations in ms (default: 5000)
// Debugging and monitoring
enableInvalidationDebugging?: boolean; // Enable detailed invalidation logging (default: false)
}
interface InvalidationRule {
// Trigger conditions - when should this rule fire?
methods: CacheMethod[]; // HTTP methods that trigger this rule ['POST', 'PUT', 'DELETE']
pathPattern: string | RegExp; // Path pattern that must match to trigger rule
condition?: (req: any) => boolean; // Optional additional condition function
// Invalidation targets - what should be invalidated?
invalidatePatterns: string[]; // Cache key patterns to invalidate when rule triggers
invalidateMethods?: CacheMethod[]; // Which cached methods to invalidate (default: ['GET'])
respectUserScope?: boolean; // Whether to only invalidate for the same user (default: true)
// Metadata for debugging and organization
name?: string; // Rule name for debugging logs
description?: string; // Human-readable rule description
}Cache key format
{method}:{normalized_url}:{sorted_query_params}:{hashed_request_body}:{userId_or_anon}
normalized_urlexcludes query string- Query params sorted alphabetically
- Request body hashed with SHA-256 for non-GET
userId_or_anonviagetUserId(req)
Invalidation Pattern Format
Patterns use the format: {method}:{path_pattern}
- Method:
GET,POST,PUT,DELETE, or*for any method - Path Pattern: URL path with optional wildcards and placeholders
*matches any characters{placeholder}gets replaced with actual values from request- Patterns are case-sensitive
Examples:
'GET:/api/users*' // All GET requests to /api/users and sub-paths
'*:/api/posts/123*' // Any method to /api/posts/123 and sub-paths
'GET:/api/users/{userId}*' // GET requests with userId placeholder
'POST:/api*' // All POST requests under /apiPerformance Considerations
- Pattern Deduplication: Identical patterns are processed only once per invalidation
- User Scoping: When enabled, only processes cache keys for the current user
- Background Processing: Invalidation happens asynchronously when possible
- Efficient Matching: Optimized wildcard and placeholder matching algorithms
- Request Coalescing: Prevents cache stampede during high concurrency
Debugging
Enable detailed logging to troubleshoot invalidation:
invalidation: {
enableInvalidationDebugging: true
}
// Logs will show:
// [PatternInvalidation] Starting pattern invalidation { requestId, method, path, userId }
// [PatternInvalidation] Processing rule { ruleName, patterns }
// [PatternInvalidation] Invalidating pattern { pattern, keysFound, keysInvalidated }
// [PatternInvalidation] Pattern invalidation completed { duration, keysProcessed }Testing
npm testReal-World Examples
E-commerce Application
app.use(apiCache({
ttl: 300, // 5 minutes
useMemory: true,
useRedis: true,
redisUrl: process.env.REDIS_URL,
invalidateOnWrite: true,
getUserId: (req) => req.user?.id,
invalidation: {
autoInvalidateRules: {
autoGenerateRestRules: true,
autoInvalidateParents: true,
autoInvalidateCollections: true
},
respectUserScope: true,
enableInvalidationDebugging: process.env.NODE_ENV === 'development'
}
}));Social Media Platform
app.use(apiCache({
ttl: 180, // 3 minutes
invalidateOnWrite: true,
getUserId: (req) => req.user?.id,
invalidation: {
autoInvalidateRules: {
autoGenerateRestRules: true,
autoInvalidateParents: true,
autoInvalidateCollections: true
},
respectUserScope: true
},
// Cache search results but not real-time data
cachePostPredicate: (req) => req.path.includes('/search'),
excludePaths: ['/api/messages', '/api/notifications/realtime']
}));Multi-tenant SaaS Application
app.use(apiCache({
ttl: 600, // 10 minutes
getUserId: (req) => `${req.tenant?.id}:${req.user?.id}`,
invalidation: {
autoInvalidateRules: {
autoGenerateRestRules: true,
autoInvalidateParents: true,
autoInvalidateCollections: true
},
respectUserScope: true, // Critical for multi-tenancy
enableInvalidationDebugging: true
},
// Custom invalidation for admin actions
getInvalidationPatterns: (req) => {
if (req.user?.role === 'admin' && req.path.includes('/admin/')) {
return [`GET:*:*:*:${req.tenant.id}:*`]; // Clear entire tenant cache
}
return [];
}
}));Invalidation Configuration Examples
Basic Auto-Generated REST Rules
invalidation: {
autoInvalidateRules: {
autoGenerateRestRules: true // Automatically handles common REST patterns
}
}
// Auto-generated behavior:
// POST /api/users → invalidates GET:/api/users*
// PUT /api/users/123 → invalidates GET:/api/users/123*, GET:/api/users*
// DELETE /api/posts/456 → invalidates GET:/api/posts/456*, GET:/api/posts*Advanced Custom Rules
invalidation: {
invalidationRules: [
{
name: 'admin-cache-clear',
description: 'Admin actions clear entire cache',
methods: ['POST', 'PUT', 'DELETE'],
pathPattern: /^\/api\/admin\//,
condition: (req) => req.user?.role === 'admin',
invalidatePatterns: ['GET:*'], // Clear everything
respectUserScope: false // Clear for all users
},
{
name: 'order-fulfillment',
description: 'Order status changes affect inventory and user data',
methods: ['PUT', 'PATCH'],
pathPattern: '/api/orders/{orderId}/status',
invalidatePatterns: [
'GET:/api/products*', // Inventory updates
'GET:/api/users/{userId}*', // User order history
'GET:/api/analytics/sales*' // Sales reports
],
invalidateMethods: ['GET', 'POST'], // Also clear cached POST searches
respectUserScope: true
},
{
name: 'content-moderation',
description: 'Content moderation affects multiple feeds',
methods: ['PUT'],
pathPattern: /^\/api\/(posts|comments)\/\d+\/moderate$/,
invalidatePatterns: [
'GET:/api/feed*', // All feeds
'GET:/api/trending*', // Trending content
'GET:/api/posts/{postId}*', // Specific post
'GET:/api/comments*' // Comment threads
]
}
],
// Performance settings
maxInvalidationDepth: 5, // Allow deeper rule chains
invalidationTimeout: 10000, // 10 second timeout
enableInvalidationDebugging: process.env.NODE_ENV === 'development'
}Redis Integration Testing
Prerequisites
- Docker: Ensure Docker is installed and running
- Redis Container: Use the provided setup script
Quick Start
# Start Redis for testing
./setup-redis-tests.sh
# Run comprehensive Redis integration tests
npm test -- --testNamePattern="redis-complete"
# Run specific test cases (optional)
npm test -- --testNamePattern="Test Case (2|6)" # Cache Hit & TTL Expiry
# Stop Redis when done
docker stop redis-test && docker rm redis-test