18 Caching Redis
title: Redis Caching in Node.js REST APIs — Complete Guide weight: 28 date: 2026-06-28 lastmod: 2026-06-28 description: Implement Redis caching in Node.js REST APIs for response caching, database query caching, session storage, and cache invalidation with TTL-based expiration. tags: [api-development, nodejs]
Redis caching in Node.js REST APIs stores frequently accessed data in memory, reducing database load and response times by caching API responses, database queries, and computed results with configurable TTL expiration.
```mermaid
flowchart TD
A[Request] --> B{In Cache?}
B -->|Yes| C[Return Cached Response]
B -->|No| D[Query Database]
D --> E[Store in Redis]
E --> F[Return Response]
C --> G[Fast Response <5ms]
F --> H[Normal Response 50ms+]
style A fill:#e1f5fe
style B fill:#fff9c4
style C fill:#c8e6c9
style D fill:#f3e5f5
Redis stores key-value pairs with optional TTL. Cache API responses using the request URL as the key and the JSON response as the value. Set appropriate TTLs based on how often data changes. Invalidate cache entries when data is mutated.
Think of Redis like a whiteboard in a busy office. Frequently referenced information (phone extensions, meeting room status) is written on the whiteboard so you do not have to go to the filing cabinet (database) every time.
Example: Redis Client Setup
const redis = require('redis');
const redisClient = redis.createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379'
});
redisClient.on('error', (err) => console.error('Redis error:', err));
redisClient.on('connect', () => console.log('Connected to Redis'));
(async () => {
await redisClient.connect();
})();
module.exports = redisClient;
Example: Caching Middleware
const redisClient = require('./redis');
const cacheMiddleware = (ttl = 60) => {
return async (req, res, next) => {
// Only cache GET requests
if (req.method !== 'GET') {
return next();
}
const key = `cache:${req.originalUrl}`;
try {
const cached = await redisClient.get(key);
if (cached) {
console.log(`Cache hit: ${key}`);
return res.json(JSON.parse(cached));
}
console.log(`Cache miss: ${key}`);
// Override res.json to cache the response
const originalJson = res.json.bind(res);
res.json = (body) => {
redisClient.setEx(key, ttl, JSON.stringify(body));
return originalJson(body);
};
next();
} catch (error) {
console.error('Cache error:', error);
next();
}
};
};
// Usage
app.get('/api/products', cacheMiddleware(300), productController.list);
Example: Cache Invalidation on Mutations
const redisClient = require('./redis');
const invalidateCache = (...patterns) => {
return async (req, res, next) => {
// Store the original json method
const originalJson = res.json.bind(res);
res.json = async (body) => {
// Invalidate cache after successful mutations
if (res.statusCode >= 200 && res.statusCode < 300) {
for (const pattern of patterns) {
const keys = await redisClient.keys(pattern);
if (keys.length > 0) {
await redisClient.del(keys);
console.log(`Invalidated ${keys.length} cache entries for pattern: ${pattern}`);
}
}
}
return originalJson(body);
};
next();
};
};
// Usage
app.post('/api/products',
invalidateCache('cache:/api/products*', 'cache:/api/categories*'),
productController.createProduct
);
Example: Database Query Caching
const redisClient = require('./redis');
const getCachedProducts = async (filters) => {
const cacheKey = `products:${JSON.stringify(filters)}`;
// Try cache
const cached = await redisClient.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Query database
const products = await Product.find(filters)
.sort('-createdAt')
.limit(50);
// Store in cache for 5 minutes
await redisClient.setEx(cacheKey, 300, JSON.stringify(products));
return products;
};
app.get('/api/products', async (req, res, next) => {
try {
const products = await getCachedProducts(req.query);
res.json({ status: 'success', data: products });
} catch (error) {
next(error);
}
});
Common Mistakes
- Caching too aggressively — Caching everything with long TTLs serves stale data. Cache selectively with appropriate TTLs based on data update frequency.
- Not invalidating cache on mutations — After create/update/delete, old cached responses remain. Always invalidate related cache entries after mutations.
- Caching user-specific data globally — Responses containing user-specific data (profile, orders) must be cached per-user, not globally.
- Using Redis as a primary database — Redis is an in-memory cache, not a durable database. Data is lost on restart. Store persistent data in a real database.
- Not handling Redis connection failures — If Redis is down, requests should fall through to the database instead of failing. Implement graceful degradation.
Practice Questions
- What is the purpose of TTL in caching?
- How do you invalidate cache entries when data changes?
- Why should you cache only GET requests?
- How do you handle Redis connection failures gracefully?
- Challenge: Implement a two-tier caching strategy with in-memory cache (node-cache) as L1 and Redis as L2. Cache popular products in memory with 60s TTL and all products in Redis with 300s TTL. Implement cache invalidation on product updates.
FAQ
Mini Project
Build a Redis caching layer for a product catalog API. Implement: cache middleware for GET endpoints, TTL configuration per endpoint, cache invalidation on mutations, per-user cache for user-specific endpoints, graceful degradation when Redis is unavailable, and cache statistics endpoint (hit rate, memory usage).
What's Next
Now learn about testing with Jest and Supertest in Building REST APIs with Node.js.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro