Skip to content

Response Caching

Learn how to implement response caching in your Veloce-TS applications to reduce latency and database load.

Veloce-TS provides a powerful caching system that allows you to cache expensive operations and reduce response times. The system supports:

  • In-Memory Store - Fast caching for single-instance applications
  • Redis Store - Distributed caching for multi-instance deployments
  • TTL Support - Flexible time-to-live configuration
  • Pattern-Based Invalidation - Invalidate related caches efficiently
  • Cache Headers - Automatic X-Cache: HIT/MISS headers
import { VeloceTS, CacheManager, MemoryCacheStore } from 'veloce-ts';
const app = new VeloceTS();
// Configure cache store
const cacheStore = new MemoryCacheStore({
maxSize: 1000, // Maximum entries
cleanupInterval: 60000 // Clean expired entries every 60s
});
CacheManager.setDefaultStore(cacheStore);
import { Controller, Get, Cache } from 'veloce-ts';
@Controller('/products')
class ProductController {
@Get('/')
@Cache({ ttl: '5m' }) // Cache for 5 minutes
async getAllProducts() {
return await db.products.findAll();
}
}

Perfect for single-instance applications or development:

import { MemoryCacheStore, CacheManager } from 'veloce-ts';
const store = new MemoryCacheStore({
maxSize: 1000, // Max entries (LRU eviction)
cleanupInterval: 60000 // Cleanup interval in ms
});
CacheManager.setDefaultStore(store);

Features:

  • Fast (< 1ms access time)
  • LRU (Least Recently Used) eviction
  • Automatic cleanup of expired entries
  • No external dependencies

For distributed applications with multiple instances:

import { createRedisCacheStore, CacheManager } from 'veloce-ts';
import { createClient } from 'redis';
// Using redis package
const client = createClient({ url: 'redis://localhost:6379' });
await client.connect();
const store = createRedisCacheStore(client, {
prefix: 'myapp:cache:' // Key prefix for namespacing
});
CacheManager.setDefaultStore(store);
// Using ioredis package
import Redis from 'ioredis';
const client = new Redis('redis://localhost:6379');
const store = createRedisCacheStore(client);

Features:

  • Distributed caching across instances
  • Persistent cache (survives restarts)
  • ~2-5ms access time
  • Supports clustering

Cache route responses automatically:

@Controller('/api')
class ApiController {
// Simple TTL
@Get('/data')
@Cache({ ttl: 300 }) // 5 minutes in seconds
async getData() {
return await fetchExpensiveData();
}
// String format TTL
@Get('/products')
@Cache({ ttl: '5m' }) // Supports: '5s', '5m', '5h', '5d'
async getProducts() {
return await db.products.findAll();
}
// Custom cache key
@Get('/user/:id')
@Cache({ ttl: '10m', key: 'user:{id}' })
async getUser(@Param('id') id: string) {
return await db.users.findById(id);
}
// Cache with query params
@Get('/search')
@Cache({
ttl: '2m',
includeQuery: true // Different cache for each query
})
async search(@Query() query: any) {
return await db.products.search(query);
}
// Conditional caching
@Get('/items')
@Cache({
ttl: '5m',
condition: (result) => result.length > 0 // Only cache if has results
})
async getItems() {
return await db.items.findAll();
}
}

Options:

interface CacheOptions {
ttl: number | string; // Time to live
key?: string; // Custom key (supports {param} placeholders)
prefix?: string; // Key prefix
includeQuery?: boolean; // Include query params in key
varyByHeaders?: string[]; // Vary by specific headers
condition?: (result) => boolean; // Cache condition
store?: CacheStore; // Custom store
}

Invalidate cache after mutations:

@Controller('/products')
class ProductController {
@Get('/')
@Cache({ ttl: '5m', key: 'products:list' })
async list() {
return await db.products.findAll();
}
@Post('/')
@CacheInvalidate('products:*') // Clear all product caches
async create(@Body() data: CreateProductDTO) {
return await db.products.create(data);
}
@Put('/:id')
@CacheInvalidate(['product:{id}', 'products:*']) // Multiple patterns
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 };
}
}

For functional API routes:

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

Cache keys are automatically generated from:

  • HTTP method
  • Route path
  • Route parameters
  • Query parameters (if includeQuery: true)
  • Headers (if varyByHeaders specified)

Example:

// GET /products/123?sort=name
// Default key: "get:/products/:id:{"id":"123"}"
// With includeQuery: "get:/products/:id:{"id":"123"}:{"sort":"name"}"

Use placeholders to create semantic keys:

@Get('/products/:id')
@Cache({ ttl: '5m', key: 'product:{id}' })
async getProduct(@Param('id') id: string) {
return await db.products.findById(id);
}
// Key becomes: "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);
}
// Key becomes: "user:456:post:789"

Namespace your cache keys:

@Cache({
ttl: '5m',
prefix: 'api:v1',
key: 'products:list'
})
// Final key: "api:v1:products:list"
// Seconds (number)
@Cache({ ttl: 300 }) // 5 minutes
// String format
@Cache({ ttl: '5s' }) // 5 seconds
@Cache({ ttl: '5m' }) // 5 minutes
@Cache({ ttl: '5h' }) // 5 hours
@Cache({ ttl: '5d' }) // 5 days
Data TypeTTLExample
Static content1d - 7dHelp pages, terms
Product catalog5m - 1hProducts, categories
User data1m - 5mUser profile
Search results30s - 2mSearch queries
Real-time data10s - 30sStock prices

Use wildcards to invalidate related caches:

// Clear all product caches
await CacheManager.invalidate('products:*');
// Clear all user caches
await CacheManager.invalidate('user:*');
// Clear specific pattern
await CacheManager.invalidate('user:123:*');
import { CacheManager, invalidateCache, deleteCache, clearCache } from 'veloce-ts';
// Invalidate by pattern
await CacheManager.invalidate('products:*');
await invalidateCache('products:*');
// Delete specific key
await CacheManager.delete('product:123');
await deleteCache('product:123');
// Clear all cache
await CacheManager.clear();
await clearCache();
import { getCache, setCache } from 'veloce-ts';
// Get from cache
const data = await getCache<Product[]>('products:list');
// Set in cache
await setCache('products:list', products, '5m');
// Delete from cache
await deleteCache('products:list');

Responses automatically include cache status:

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

or

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

Good candidates for caching:

  • Product catalogs
  • User profiles (when not frequently updated)
  • Search results
  • Aggregated data
  • External API responses

Don’t cache:

  • User-specific sensitive data
  • Real-time data (unless very short TTL)
  • Data that changes frequently
// Static data - Long TTL
@Cache({ ttl: '1d' })
async getTermsOfService() { ... }
// Dynamic data - Short TTL
@Cache({ ttl: '30s' })
async getCurrentPrice() { ... }
// User data - Medium TTL
@Cache({ ttl: '5m' })
async getUserProfile() { ... }

Always invalidate related caches after updates:

@Post('/')
@CacheInvalidate('products:*')
async createProduct() { ... }
@Put('/:id')
@CacheInvalidate(['product:{id}', 'products:*'])
async updateProduct() { ... }
// Good - Specific pattern
@CacheInvalidate('products:category:electronics')
// Better - More specific
@CacheInvalidate('products:category:electronics:page:*')
// Too broad - May clear unrelated caches
@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
});

For multi-instance deployments:

// Development - Use Memory Store
if (process.env.NODE_ENV === 'development') {
CacheManager.setDefaultStore(new MemoryCacheStore());
}
// Production - Use 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';
// Configure cache
const cacheStore = new MemoryCacheStore({ maxSize: 1000 });
CacheManager.setDefaultStore(cacheStore);
const ProductSchema = z.object({
name: z.string(),
price: z.number()
});
@Controller('/products')
class ProductController {
// List - cached for 5 minutes
@Get('/')
@Cache({ ttl: '5m', key: 'products:list' })
async list() {
return await db.products.findAll();
}
// Get by ID - cached for 10 minutes
@Get('/:id')
@Cache({ ttl: '10m', key: 'product:{id}' })
async getById(@Param('id') id: string) {
return await db.products.findById(id);
}
// Search - cached by query params
@Get('/search')
@Cache({ ttl: '2m', includeQuery: true })
async search(@Query() query: any) {
return await db.products.search(query);
}
// Create - invalidates list cache
@Post('/')
@CacheInvalidate('products:*')
async create(@Body(ProductSchema) data: z.infer<typeof ProductSchema>) {
return await db.products.create(data);
}
// Update - invalidates specific product and list
@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);
}
// Delete - invalidates all related caches
@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);