Caché de Respuestas
Caché de Respuestas
Sección titulada «Caché de Respuestas»Aprende cómo implementar caché de respuestas en tus aplicaciones Veloce-TS para reducir latencia y carga en la base de datos.
Tabla de Contenidos
Sección titulada «Tabla de Contenidos»- Resumen
- Inicio Rápido
- Almacenes de Caché
- Decoradores
- Middleware
- Claves de Caché
- Configuración de TTL
- Invalidación de Caché
- Mejores Prácticas
Resumen
Sección titulada «Resumen»Veloce-TS proporciona un poderoso sistema de caché que te permite cachear operaciones costosas y reducir tiempos de respuesta. El sistema soporta:
- Almacén en Memoria - Caché rápido para aplicaciones de instancia única
- Almacén Redis - Caché distribuido para despliegues multi-instancia
- Soporte TTL - Configuración flexible de tiempo de vida
- Invalidación Basada en Patrones - Invalidar cachés relacionados eficientemente
- Headers de Caché - Headers automáticos
X-Cache: HIT/MISS
Inicio Rápido
Sección titulada «Inicio Rápido»1. Configuración Básica
Sección titulada «1. Configuración Básica»import { VeloceTS, CacheManager, MemoryCacheStore } from 'veloce-ts';
const app = new VeloceTS();
// Configurar almacén de cachéconst cacheStore = new MemoryCacheStore({ maxSize: 1000, // Máximo de entradas cleanupInterval: 60000 // Limpiar entradas expiradas cada 60s});
CacheManager.setDefaultStore(cacheStore);2. Cachear una Ruta
Sección titulada «2. Cachear una Ruta»import { Controller, Get, Cache } from 'veloce-ts';
@Controller('/products')class ProductController { @Get('/') @Cache({ ttl: '5m' }) // Cachear por 5 minutos async getAllProducts() { return await db.products.findAll(); }}Almacenes de Caché
Sección titulada «Almacenes de Caché»Almacén en Memoria
Sección titulada «Almacén en Memoria»Perfecto para aplicaciones de instancia única o desarrollo:
import { MemoryCacheStore, CacheManager } from 'veloce-ts';
const store = new MemoryCacheStore({ maxSize: 1000, // Max entradas (expulsión LRU) cleanupInterval: 60000 // Intervalo de limpieza en ms});
CacheManager.setDefaultStore(store);Características:
- Rápido (< 1ms tiempo de acceso)
- Expulsión LRU real (Menos Recientemente Usado)
- Limpieza automática de entradas expiradas
- Sin dependencias externas
Llama a store.destroy() (o CacheManager.destroy(), más abajo) cuando termines con un almacén: cleanupInterval programa un timer que, si no, mantiene vivo el event loop e impide que el proceso salga.
Almacén Redis
Sección titulada «Almacén Redis»Para aplicaciones distribuidas con múltiples instancias:
import { createRedisCacheStore, CacheManager } from 'veloce-ts';import { createClient } from 'redis';
// Usando paquete redisconst client = createClient({ url: 'redis://localhost:6379' });await client.connect();
const store = createRedisCacheStore(client, { prefix: 'myapp:cache:' // Prefijo de clave para namespacing});
CacheManager.setDefaultStore(store);// Usando paquete ioredisimport Redis from 'ioredis';
const client = new Redis('redis://localhost:6379');const store = createRedisCacheStore(client);Características:
- Caché distribuido entre instancias
- Caché persistente (sobrevive reinicios)
- ~2-5ms tiempo de acceso
- Soporta clustering
- Enumeración de claves sin bloqueo
Liberar almacenes
Sección titulada «Liberar almacenes»CacheManager.destroy() libera los recursos de todos los almacenes registrados (timers, conexiones) — úsalo junto al apagado gradual:
app.onShutdown(async () => { CacheManager.destroy();});CacheManager.reset() (útil entre suites de tests) ahora destruye los almacenes antes de reemplazarlos. Antes soltaba la referencia sin limpiar el intervalo de limpieza del almacén anterior, así que llamadas repetidas acumulaban timers y podían mantener un proceso vivo indefinidamente.
Decoradores
Sección titulada «Decoradores»@Cache()
Sección titulada «@Cache()»:::danger Las rutas cacheadas por usuario necesitan una clave que distinga al llamante
La clave por defecto es método + ruta + params. Si la respuesta depende de quién pregunta,
indícalo con varyByHeaders o keyGenerator; si no, la respuesta del primer llamante se sirve
a todos los demás:
@Get('/me')@Cache({ ttl: '1m', varyByHeaders: ['authorization'] })async me(@CurrentUser() user: TokenPayload) { … }
// o construye la clave con el id de usuario@Cache({ ttl: '1m', keyGenerator: (c) => `me:${c.get('auth.user')?.sub}` })Desde 3.0.0, cachear una ruta que lee @CurrentUser(), @Token() o datos de sesión con una
clave independiente del llamante emite un aviso al compilar las rutas.
:::
Cachear respuestas de rutas automáticamente:
@Controller('/api')class ApiController { // TTL simple @Get('/data') @Cache({ ttl: 300 }) // 5 minutos en segundos async getData() { return await fetchExpensiveData(); }
// TTL en formato string @Get('/products') @Cache({ ttl: '5m' }) // Soporta: '5s', '5m', '5h', '5d' async getProducts() { return await db.products.findAll(); }
// Clave de caché personalizada @Get('/user/:id') @Cache({ ttl: '10m', key: 'user:{id}' }) async getUser(@Param('id') id: string) { return await db.users.findById(id); }
// Caché con parámetros de query @Get('/search') @Cache({ ttl: '2m', includeQuery: true // Caché diferente para cada query }) async search(@Query() query: any) { return await db.products.search(query); }
// Caché condicional @Get('/items') @Cache({ ttl: '5m', condition: (result) => result.length > 0 // Solo cachear si hay resultados }) async getItems() { return await db.items.findAll(); }}Opciones:
interface CacheOptions { ttl: number | string; // Tiempo de vida key?: string; // Clave personalizada (soporta placeholders {param}) prefix?: string; // Prefijo de clave includeQuery?: boolean; // Incluir parámetros query en clave varyByHeaders?: string[]; // Varía la clave según estos headers keyGenerator?: (c) => string; // Construye la clave tú (reemplaza el esquema por defecto) condition?: (result) => boolean; // Condición de caché store?: CacheStore; // Almacén personalizado}@CacheInvalidate()
Sección titulada «@CacheInvalidate()»Invalidar caché después de mutaciones:
@Controller('/products')class ProductController { @Get('/') @Cache({ ttl: '5m', key: 'products:list' }) async list() { return await db.products.findAll(); }
@Post('/') @CacheInvalidate('products:*') // Limpiar todos los cachés de productos async create(@Body() data: CreateProductDTO) { return await db.products.create(data); }
@Put('/:id') @CacheInvalidate(['product:{id}', 'products:*']) // Múltiples patrones async update(@Param('id') id: string, @Body() data: UpdateProductDTO) { return await db.products.update(id, data); }
@Delete('/:id') @CacheInvalidate([ 'product:{id}', 'products:*', 'featured:*' ]) async delete(@Param('id') id: string) { await db.products.delete(id); return { success: true }; }}Middleware
Sección titulada «Middleware»Para rutas de API funcional:
Middleware de Caché
Sección titulada «Middleware de Caché»import { createCacheMiddleware } from 'veloce-ts';
app.get('/products', { middleware: [createCacheMiddleware({ ttl: '5m' })], handler: async () => { return await db.products.findAll(); }});
// Con clave personalizadaapp.get('/user/:id/posts', { middleware: [createCacheMiddleware({ ttl: 300, key: 'user:{id}:posts' })], handler: async (c) => { const id = c.req.param('id'); return await db.posts.findByUser(id); }});Middleware de Invalidación de Caché
Sección titulada «Middleware de Invalidación de Caché»import { createCacheInvalidationMiddleware } from 'veloce-ts';
app.post('/products', { middleware: [createCacheInvalidationMiddleware('products:*')], handler: async (c) => { const body = await c.req.json(); return await db.products.create(body); }});
// Múltiples patronesapp.put('/products/:id', { middleware: [createCacheInvalidationMiddleware([ 'product:{id}', 'products:*' ])], handler: async (c) => { const id = c.req.param('id'); const body = await c.req.json(); return await db.products.update(id, body); }});Claves de Caché
Sección titulada «Claves de Caché»Generación Automática
Sección titulada «Generación Automática»Las claves de caché se generan automáticamente desde:
- Método HTTP
- Ruta de la ruta
- Parámetros de ruta
- Parámetros query (si
includeQuery: true) - Headers (si
varyByHeadersespecificado)
Ejemplo:
// GET /products/123?sort=name// Clave por defecto: "get:/products/:id:{"id":"123"}"// Con includeQuery: "get:/products/:id:{"id":"123"}:{"sort":"name"}"Claves Personalizadas con Placeholders
Sección titulada «Claves Personalizadas con Placeholders»Usa placeholders para crear claves semánticas:
@Get('/products/:id')@Cache({ ttl: '5m', key: 'product:{id}' })async getProduct(@Param('id') id: string) { return await db.products.findById(id);}// La clave se convierte en: "product:123"
@Get('/user/:userId/posts/:postId')@Cache({ ttl: '5m', key: 'user:{userId}:post:{postId}' })async getPost(@Param('userId') userId: string, @Param('postId') postId: string) { return await db.posts.find(userId, postId);}// La clave se convierte en: "user:456:post:789"Prefijos de Clave
Sección titulada «Prefijos de Clave»Organiza tus claves de caché con namespaces:
@Cache({ ttl: '5m', prefix: 'api:v1', key: 'products:list'})// Clave final: "api:v1:products:list"Configuración de TTL
Sección titulada «Configuración de TTL»Formatos
Sección titulada «Formatos»// Segundos (número)@Cache({ ttl: 300 }) // 5 minutos
// Formato string@Cache({ ttl: '5s' }) // 5 segundos@Cache({ ttl: '5m' }) // 5 minutos@Cache({ ttl: '5h' }) // 5 horas@Cache({ ttl: '5d' }) // 5 díasRecomendaciones
Sección titulada «Recomendaciones»| Tipo de Dato | TTL | Ejemplo |
|---|---|---|
| Contenido estático | 1d - 7d | Páginas de ayuda, términos |
| Catálogo de productos | 5m - 1h | Productos, categorías |
| Datos de usuario | 1m - 5m | Perfil de usuario |
| Resultados de búsqueda | 30s - 2m | Queries de búsqueda |
| Datos en tiempo real | 10s - 30s | Precios de acciones |
Invalidación de Caché
Sección titulada «Invalidación de Caché»Coincidencia de Patrones
Sección titulada «Coincidencia de Patrones»Usa comodines para invalidar cachés relacionados:
// Limpiar todos los cachés de productosawait CacheManager.invalidate('products:*');
// Limpiar todos los cachés de usuarioawait CacheManager.invalidate('user:*');
// Limpiar patrón específicoawait CacheManager.invalidate('user:123:*');Invalidación Manual
Sección titulada «Invalidación Manual»import { CacheManager, invalidateCache, deleteCache, clearCache } from 'veloce-ts';
// Invalidar por patrónawait CacheManager.invalidate('products:*');await invalidateCache('products:*');
// Eliminar clave específicaawait CacheManager.delete('product:123');await deleteCache('product:123');
// Limpiar todo el cachéawait CacheManager.clear();await clearCache();Caché Programático
Sección titulada «Caché Programático»import { getCache, setCache } from 'veloce-ts';
// Obtener del cachéconst data = await getCache<Product[]>('products:list');
// Establecer en cachéawait setCache('products:list', products, '5m');
// Eliminar del cachéawait deleteCache('products:list');Headers de Caché
Sección titulada «Headers de Caché»Las respuestas incluyen automáticamente el estado del caché:
HTTP/1.1 200 OKX-Cache: HITX-Request-ID: abc-123-def-456Content-Type: application/jsono
HTTP/1.1 200 OKX-Cache: MISSX-Request-ID: abc-123-def-456Content-Type: application/jsonMejores Prácticas
Sección titulada «Mejores Prácticas»1. Cachear Operaciones de Lectura Intensiva
Sección titulada «1. Cachear Operaciones de Lectura Intensiva»✅ Buenos candidatos para caché:
- Catálogos de productos
- Perfiles de usuario (cuando no se actualizan frecuentemente)
- Resultados de búsqueda
- Datos agregados
- Respuestas de APIs externas
❌ No cachear:
- Datos sensibles específicos del usuario
- Datos en tiempo real (a menos que TTL muy corto)
- Datos que cambian frecuentemente
2. Usar TTL Apropiado
Sección titulada «2. Usar TTL Apropiado»// Datos estáticos - TTL largo@Cache({ ttl: '1d' })async getTermsOfService() { ... }
// Datos dinámicos - TTL corto@Cache({ ttl: '30s' })async getCurrentPrice() { ... }
// Datos de usuario - TTL medio@Cache({ ttl: '5m' })async getUserProfile() { ... }3. Invalidar en Mutaciones
Sección titulada «3. Invalidar en Mutaciones»Siempre invalida cachés relacionados después de actualizaciones:
@Post('/')@CacheInvalidate('products:*')async createProduct() { ... }
@Put('/:id')@CacheInvalidate(['product:{id}', 'products:*'])async updateProduct() { ... }4. Usar Coincidencia de Patrones Sabiamente
Sección titulada «4. Usar Coincidencia de Patrones Sabiamente»// Bien - Patrón específico@CacheInvalidate('products:category:electronics')
// Mejor - Más específico@CacheInvalidate('products:category:electronics:page:*')
// Demasiado amplio - Puede limpiar cachés no relacionados@CacheInvalidate('*')5. Monitorear Rendimiento del Caché
Sección titulada «5. Monitorear Rendimiento del Caché»const store = CacheManager.getDefaultStore() as MemoryCacheStore;const stats = store.getStats();console.log({ size: stats.size, maxSize: stats.maxSize, utilization: (stats.size / stats.maxSize) * 100});6. Usar Redis para Producción
Sección titulada «6. Usar Redis para Producción»Para despliegues multi-instancia:
// Desarrollo - Usar Almacén en Memoriaif (process.env.NODE_ENV === 'development') { CacheManager.setDefaultStore(new MemoryCacheStore());}
// Producción - Usar Redisif (process.env.NODE_ENV === 'production') { const redis = createClient({ url: process.env.REDIS_URL }); await redis.connect(); CacheManager.setDefaultStore(createRedisCacheStore(redis));}Ejemplo Completo
Sección titulada «Ejemplo Completo»import { VeloceTS, Controller, Get, Post, Put, Delete, Param, Body, Cache, CacheInvalidate, CacheManager, MemoryCacheStore} from 'veloce-ts';import { z } from 'zod';
// Configurar cachéconst cacheStore = new MemoryCacheStore({ maxSize: 1000 });CacheManager.setDefaultStore(cacheStore);
const ProductSchema = z.object({ name: z.string(), price: z.number()});
@Controller('/products')class ProductController { // Listar - cacheado por 5 minutos @Get('/') @Cache({ ttl: '5m', key: 'products:list' }) async list() { return await db.products.findAll(); }
// Obtener por ID - cacheado por 10 minutos @Get('/:id') @Cache({ ttl: '10m', key: 'product:{id}' }) async getById(@Param('id') id: string) { return await db.products.findById(id); }
// Buscar - cacheado por parámetros query @Get('/search') @Cache({ ttl: '2m', includeQuery: true }) async search(@Query() query: any) { return await db.products.search(query); }
// Crear - invalida caché de lista @Post('/') @CacheInvalidate('products:*') async create(@Body(ProductSchema) data: z.infer<typeof ProductSchema>) { return await db.products.create(data); }
// Actualizar - invalida producto específico y lista @Put('/:id') @CacheInvalidate(['product:{id}', 'products:*']) async update( @Param('id') id: string, @Body(ProductSchema) data: z.infer<typeof ProductSchema> ) { return await db.products.update(id, data); }
// Eliminar - invalida todos los cachés relacionados @Delete('/:id') @CacheInvalidate(['product:{id}', 'products:*']) async delete(@Param('id') id: string) { await db.products.delete(id); return { success: true }; }}
const app = new VeloceTS();app.include(ProductController);app.listen(3000);Próximos Pasos
Sección titulada «Próximos Pasos»- Aprende sobre Contexto de Request para rastreo de peticiones
- Explora Logging para depurar comportamiento del caché
- Compara el overhead del framework en la suite de benchmarks publicada