Cache con Redis
Redis mejora el rendimiento almacenando datos en memoria.
Instalación
bash
npm install ioredisDocker
bash
docker run --name redis -p 6379:6379 -d redis:7-alpineConfiguración
typescript
// src/config/redis.tsimport Redis from 'ioredis'; export const redis = new Redis({ host: process.env.REDIS_HOST || 'localhost', port: Number(process.env.REDIS_PORT) || 6379, password: process.env.REDIS_PASSWORD,}); redis.on('connect', () => console.log('Redis conectado'));redis.on('error', (err) => console.error('Error Redis:', err));Cache Service
typescript
// src/services/cache.service.tsimport { redis } from '../config/redis'; const DEFAULT_TTL = 3600; // 1 hora export class CacheService { async get<T>(key: string): Promise<T | null> { const data = await redis.get(key); return data ? JSON.parse(data) : null; } async set(key: string, value: unknown, ttl: number = DEFAULT_TTL) { await redis.set(key, JSON.stringify(value), 'EX', ttl); } async del(key: string) { await redis.del(key); } async delPattern(pattern: string) { const keys = await redis.keys(pattern); if (keys.length > 0) { await redis.del(...keys); } }} export const cacheService = new CacheService();Uso en Services
typescript
// src/services/product.service.tsimport { cacheService } from './cache.service'; const CACHE_KEY = 'products';const CACHE_TTL = 1800; // 30 minutos export class ProductService { async findAll(params: FindAllParams) { const cacheKey = `${CACHE_KEY}:${JSON.stringify(params)}`; // Intentar obtener del cache const cached = await cacheService.get(cacheKey); if (cached) { return cached; } // Buscar en BD const result = await this.queryProducts(params); // Guardar en cache await cacheService.set(cacheKey, result, CACHE_TTL); return result; } async update(id: string, data: UpdateDTO) { const product = await prisma.product.update({ where: { id }, data, }); // Invalidar cache await cacheService.delPattern(`${CACHE_KEY}:*`); return product; }}Middleware de Cache
typescript
// src/middlewares/cache.middleware.tsimport { Request, Response, NextFunction } from 'express';import { cacheService } from '../services/cache.service'; export const cacheMiddleware = (ttl: number = 3600) => { return async (req: Request, res: Response, next: NextFunction) => { if (req.method !== 'GET') { return next(); } const key = `route:${req.originalUrl}`; const cached = await cacheService.get(key); if (cached) { return res.json(cached); } // Interceptar res.json const originalJson = res.json.bind(res); res.json = (data: unknown) => { cacheService.set(key, data, ttl); return originalJson(data); }; next(); };}; // Usorouter.get('/', cacheMiddleware(1800), controller.index);Rate Limiting
typescript
// src/middlewares/rate-limit.middleware.tsimport { Request, Response, NextFunction } from 'express';import { redis } from '../config/redis';import { AppError } from '../errors/AppError'; export const rateLimit = (maxRequests: number, windowSeconds: number) => { return async (req: Request, res: Response, next: NextFunction) => { const ip = req.ip; const key = `ratelimit:${ip}`; const requests = await redis.incr(key); if (requests === 1) { await redis.expire(key, windowSeconds); } if (requests > maxRequests) { throw new AppError('Demasiadas solicitudes', 429); } next(); };}; // Uso: 100 requests por minutoapp.use(rateLimit(100, 60));Resumen
- ✅ Redis conectado con ioredis
- ✅ Cache service reutilizable
- ✅ Invalidación de cache
- ✅ Rate limiting
Próxima clase: Pruebas Unitarias! 🚀