Saltearse al contenido

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.

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
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);
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();
}
}

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.

Para aplicaciones distribuidas con múltiples instancias:

import { createRedisCacheStore, CacheManager } from 'veloce-ts';
import { createClient } from 'redis';
// Usando paquete redis
const 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 ioredis
import 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

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.

:::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
}

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 };
}
}

Para rutas de API funcional:

import { createCacheMiddleware } from 'veloce-ts';
app.get('/products', {
middleware: [createCacheMiddleware({ ttl: '5m' })],
handler: async () => {
return await db.products.findAll();
}
});
// Con clave personalizada
app.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);
}
});
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 patrones
app.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);
}
});

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 varyByHeaders especificado)

Ejemplo:

// GET /products/123?sort=name
// Clave por defecto: "get:/products/:id:{"id":"123"}"
// Con includeQuery: "get:/products/:id:{"id":"123"}:{"sort":"name"}"

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"

Organiza tus claves de caché con namespaces:

@Cache({
ttl: '5m',
prefix: 'api:v1',
key: 'products:list'
})
// Clave final: "api:v1:products:list"
// 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ías
Tipo de DatoTTLEjemplo
Contenido estático1d - 7dPáginas de ayuda, términos
Catálogo de productos5m - 1hProductos, categorías
Datos de usuario1m - 5mPerfil de usuario
Resultados de búsqueda30s - 2mQueries de búsqueda
Datos en tiempo real10s - 30sPrecios de acciones

Usa comodines para invalidar cachés relacionados:

// Limpiar todos los cachés de productos
await CacheManager.invalidate('products:*');
// Limpiar todos los cachés de usuario
await CacheManager.invalidate('user:*');
// Limpiar patrón específico
await CacheManager.invalidate('user:123:*');
import { CacheManager, invalidateCache, deleteCache, clearCache } from 'veloce-ts';
// Invalidar por patrón
await CacheManager.invalidate('products:*');
await invalidateCache('products:*');
// Eliminar clave específica
await CacheManager.delete('product:123');
await deleteCache('product:123');
// Limpiar todo el caché
await CacheManager.clear();
await clearCache();
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');

Las respuestas incluyen automáticamente el estado del caché:

HTTP/1.1 200 OK
X-Cache: HIT
X-Request-ID: abc-123-def-456
Content-Type: application/json

o

HTTP/1.1 200 OK
X-Cache: MISS
X-Request-ID: abc-123-def-456
Content-Type: application/json

✅ 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
// 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() { ... }

Siempre invalida cachés relacionados después de actualizaciones:

@Post('/')
@CacheInvalidate('products:*')
async createProduct() { ... }
@Put('/:id')
@CacheInvalidate(['product:{id}', 'products:*'])
async updateProduct() { ... }
// 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('*')
const store = CacheManager.getDefaultStore() as MemoryCacheStore;
const stats = store.getStats();
console.log({
size: stats.size,
maxSize: stats.maxSize,
utilization: (stats.size / stats.maxSize) * 100
});

Para despliegues multi-instancia:

// Desarrollo - Usar Almacén en Memoria
if (process.env.NODE_ENV === 'development') {
CacheManager.setDefaultStore(new MemoryCacheStore());
}
// Producción - Usar Redis
if (process.env.NODE_ENV === 'production') {
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
CacheManager.setDefaultStore(createRedisCacheStore(redis));
}
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);