Aller au contenu principal

Caching System

Learn how to leverage the powerful Redis caching layer for ultra-fast API responses.

Overview

Express Base API includes a sophisticated caching system that:

  • Reduces response time by 50-100x
  • 🔄 Dual backend - Redis (primary) with in-memory fallback
  • 🎯 Automatic invalidation on data changes
  • 📊 Built-in statistics to monitor performance
  • 🛡️ Type-safe with full TypeScript support

Architecture

Request → Cache Check → Cache Hit? → Response (< 1ms)

Cache Miss

Database Query → Cache Result → Response

Basic Usage

In Services

All services extending BaseService have caching methods available:

import BaseService from '../../../utils/bases/base.service';

class ProductService extends BaseService<IProduct> {
async getProduct(id: string) {
// Find by ID with automatic caching (1 hour TTL)
return await this.findByIdCached(id, {}, null, 3600);
}

async getFeaturedProducts() {
// Cache-aside pattern
return await this.cacheGetOrSet(
'products:featured',
async () => {
return await this.find(
{ featured: true } as any,
{ sort: { views: -1 }, limit: 10 }
);
},
3600 // 1 hour TTL
);
}
}

Manual Caching

// Set cache
await service.cacheSet('user:123', userData, 3600); // 1 hour

// Get from cache
const user = await service.cacheGet('user:123');

// Delete from cache
await service.cacheDelete('user:123');

// Delete by pattern
await service.cacheDeletePattern('user:*');

Cache Methods Reference

Protected Methods (available in services)

cacheGet<T>(key: string): Promise<T | null>

Get value from cache.

const product = await this.cacheGet<IProduct>('product:123');
if (product) {
return { error: false, data: product };
}

cacheSet(key: string, value: any, ttl: number): Promise<boolean>

Store value in cache with TTL (Time-To-Live) in seconds.

await this.cacheSet('product:123', productData, 3600); // 1 hour

cacheDelete(key: string): Promise<boolean>

Remove specific cache entry.

await this.cacheDelete('product:123');

cacheDeletePattern(pattern: string): Promise<number>

Remove multiple entries matching a pattern.

const deleted = await this.cacheDeletePattern('product:*');
console.log(`Deleted ${deleted} cache entries`);

cacheGetOrSet<T>(key: string, fetcher: Function, ttl: number): Promise<T | null>

Cache-aside pattern - get from cache or execute fetcher and cache result.

const products = await this.cacheGetOrSet(
'products:category:electronics',
async () => await this.find({ category: 'electronics' } as any),
3600
);

Public Methods

findByIdCached(id, query, model, ttl): Promise<QueryResult<T>>

Find by ID with automatic caching.

const result = await productService.findByIdCached(
'123',
{ populate: 'category' },
null,
3600
);

invalidateCache(id, model): Promise<boolean>

Invalidate cache for a specific document.

await productService.update(productId, updateData);
await productService.invalidateCache(productId);

invalidateAllCache(model): Promise<number>

Invalidate all cache entries for a model.

const deleted = await productService.invalidateAllCache();

Authentication Caching

The authentication system automatically caches user data for blazing-fast subsequent requests.

How It Works

  1. On Login - User + Auth data cached (1-hour TTL)
  2. On Authentication - Middleware checks cache first (< 1ms)
  3. On Logout/Password Reset - Cache automatically invalidated

Performance Impact

OperationWithout CacheWith CacheImprovement
Auth check50-100ms< 1ms50-100x faster
API requestDB query each timeCache hitMassive DB load reduction

Example Flow

// 1. User logs in
POST /auth/login
// → User data cached for 1 hour (key: user:auth:{userId})

// 2. Subsequent authenticated requests
GET /api/profile
// → Middleware checks cache (< 1ms)
// → No DB query needed! ✅

// 3. User logs out
POST /auth/logout
// → Cache invalidated
// → Next request requires DB query

Cache Statistics

Monitor cache performance:

import cacheService from './utils/helpers/cache.helper';

const stats = cacheService.getStats();
console.log(stats);

Output:

{
"hits": 1500,
"misses": 250,
"sets": 250,
"deletes": 50,
"errors": 0,
"hitRate": "85.71%",
"backend": "redis",
"size": undefined
}

Reset Statistics

cacheService.resetStats();

Best Practices

1. Choose Appropriate TTL

// Short-lived data (user session)
await this.cacheSet('session:123', session, 3600); // 1 hour

// Medium-lived data (product listings)
await this.cacheSet('products:featured', products, 86400); // 1 day

// Long-lived data (static content)
await this.cacheSet('config:site', config, 604800); // 1 week

2. Use Descriptive Keys

// ✅ Good - Clear and hierarchical
'user:123'
'user:123:profile'
'products:category:electronics'
'products:featured:2024-01-15'

// ❌ Bad - Unclear and hard to invalidate
'u123'
'temp_data'
'cache_12345'

3. Invalidate on Updates

async updateProduct(id: string, data: any) {
// Update database
const result = await this.update({ _id: id } as any, data);

if (!result.error) {
// Invalidate specific cache
await this.invalidateCache(id);

// Invalidate related caches
await this.cacheDelete('products:featured');
await this.cacheDeletePattern(`products:category:*`);
}

return result;
}

4. Handle Cache Miss Gracefully

async getProduct(id: string) {
// Try cache first
const cached = await this.cacheGet<IProduct>(`product:${id}`);
if (cached) {
return { error: false, data: cached };
}

// Cache miss - query database
const result = await this.findById(id);

// Cache for next time
if (!result.error && result.data) {
await this.cacheSet(`product:${id}`, result.data, 3600);
}

return result;
}

Advanced Patterns

Cache Warming

Pre-populate cache with frequently accessed data:

async warmCache() {
// Get most popular products
const popular = await this.find(
{} as any,
{ sort: { views: -1 }, limit: 100 }
);

// Cache each product
if (!popular.error && popular.data) {
for (const product of popular.data) {
await this.cacheSet(
`product:${product._id}`,
product,
3600
);
}
}
}

Conditional Caching

Cache based on conditions:

async getProducts(filters: any, useCache: boolean = true) {
const cacheKey = `products:${JSON.stringify(filters)}`;

if (useCache) {
const cached = await this.cacheGet(cacheKey);
if (cached) return { error: false, data: cached };
}

const result = await this.find(filters);

if (useCache && !result.error) {
await this.cacheSet(cacheKey, result.data, 3600);
}

return result;
}

Cache Tags

Invalidate multiple related caches:

async createProduct(data: any) {
const result = await this.create(data);

if (!result.error) {
// Invalidate all product-related caches
await this.cacheDeletePattern('products:*');
await this.cacheDeletePattern(`category:${data.category}:*`);
}

return result;
}

Troubleshooting

Cache Not Working

  1. Check Redis connection:

    redis-cli ping
    # Should respond: PONG
  2. Verify configuration:

    # In .env
    USE_QUEUE=yes
    REDIS_HOST=localhost
    REDIS_PORT=6379
  3. Check logs:

    ✅ Cache service using Redis
    # or
    ⚠️ Cache service falling back to in-memory cache

Low Hit Rate

  • TTL too short - increase cache duration
  • Keys changing too often - use stable cache keys
  • Cache invalidated too aggressively - review invalidation logic

Memory Issues

Monitor Redis memory:

redis-cli INFO memory

Set max memory policy in Redis config:

maxmemory 256mb
maxmemory-policy allkeys-lru

Configuration

Redis Configuration

configs/redis.config.ts
const redisOptions = {
host: configs.getValue('REDIS_HOST'),
port: Number(configs.getValue('REDIS_PORT')),
password: configs.getValue('REDIS_PASSWORD', false),
maxRetriesPerRequest: 3,
retryStrategy: (times) => Math.min(times * 50, 2000),
};

Cache Service Configuration

Default TTL and other settings can be customized in utils/helpers/cache.helper.ts.

Next Steps


Questions? Check the GitHub Discussions or open an issue.