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 (Menos Recientemente Usado)
  • Limpieza automática de entradas expiradas
  • Sin dependencias externas

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

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[]; // Variar por headers específicos
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);