Response Cache
Rezo includes a built-in ResponseCache that stores HTTP responses in an LRU (Least Recently Used) cache. It supports memory-only and disk-persistent modes, respects Cache-Control directives, performs conditional GET revalidation with ETag and Last-Modified, and integrates with the beforeCache hook for custom caching logic.
Quick Start
import { Rezo } from 'rezo';
// Enable caching with defaults (memory only, 30 min TTL, 500 entries)
const client = new Rezo({
cache: true
});
// First request: fetches from server, caches response
const { data } = await client.get('https://api.example.com/users');
// Second request: served from cache (no network request)
const { data: cached } = await client.get('https://api.example.com/users'); Configuration
The cache option on the Rezo constructor accepts several formats.
Boolean
cache: true enables both response caching and DNS caching with default settings:
const client = new Rezo({ cache: true }); Object (Fine-Grained Control)
const client = new Rezo({
cache: {
response: {
enable: true,
ttl: 600000, // 10 minutes
maxEntries: 1000,
methods: ['GET', 'HEAD'],
respectHeaders: true,
cacheDir: '/tmp/rezo-cache' // Enable disk persistence
},
dns: true // Also enable DNS caching
}
}); ResponseCacheConfig
interface ResponseCacheConfig {
/** Enable or disable the cache */
enable?: boolean;
/** Directory path for disk persistence (omit for memory-only) */
cacheDir?: string;
/** Whether to check network before serving cached responses */
networkCheck?: boolean;
/** Time-to-live for cached entries in milliseconds */
ttl?: number;
/** Maximum number of entries in the LRU cache */
maxEntries?: number;
/** HTTP methods to cache */
methods?: string[];
/** Whether to respect Cache-Control headers from responses */
respectHeaders?: boolean;
} Defaults
| Option | Default | Description |
|---|---|---|
enable | true | Cache is active |
ttl | 3000000 (30 min) | Time-to-live per entry |
maxEntries | 500 | LRU cache capacity |
methods | ['GET', 'HEAD'] | Only cache safe methods |
respectHeaders | true | Obey Cache-Control directives |
cacheDir | undefined | Memory-only (no disk) |
Memory and Disk Persistence
Memory-Only (Default)
When no cacheDir is specified, all cached responses live in memory. The LRU cache evicts the least recently used entries when maxEntries is exceeded.
const client = new Rezo({
cache: { response: { enable: true, maxEntries: 200 } }
}); Disk Persistence
When cacheDir is provided, the cache persists entries to disk as JSON files. This allows cached responses to survive process restarts.
const client = new Rezo({
cache: {
response: {
enable: true,
cacheDir: '/var/cache/rezo',
ttl: 86400000 // 24 hours
}
}
}); Disk persistence works as follows:
- On eviction: When an entry is evicted from the in-memory LRU (due to capacity), it is written to disk as
{base64url-encoded-key}.json - On cache miss: If an entry is not in memory, Rezo checks disk. If found and not expired, it is loaded back into memory.
- On startup: Existing disk cache files are loaded into memory. Expired files are deleted.
- Non-blocking: Disk writes are fire-and-forget (async, errors silently ignored) to avoid slowing down requests.
isCacheable()
The isCacheable() method determines whether a response should be cached based on the current configuration:
const cache = new ResponseCache({
enable: true,
methods: ['GET', 'HEAD'],
respectHeaders: true
});
cache.isCacheable('GET', 200); // true
cache.isCacheable('POST', 200); // false (POST not in methods)
cache.isCacheable('GET', 404); // false (non-2xx status)
cache.isCacheable('GET', 200, { // false (no-store directive)
'cache-control': 'no-store'
}); Rules checked in order:
- Is caching enabled?
- Is the HTTP method in the
methodslist? - Is the status code 2xx (200-299)?
- If
respectHeaders: true, doesCache-Controlcontainno-store?
Conditional GET (ETag / Last-Modified)
The response cache supports HTTP conditional requests for efficient revalidation. When a cached response contains ETag or Last-Modified headers, subsequent requests include validation headers:
How It Works
First request: Server responds with
ETag: "abc123"andLast-Modified: Thu, 01 Jan 2026 00:00:00 GMT. Rezo caches the full response.Revalidation request: Rezo sends
If-None-Match: "abc123"andIf-Modified-Since: Thu, 01 Jan 2026 00:00:00 GMT.304 Not Modified: Server confirms the cached version is still valid. Rezo updates the cache TTL and returns the cached data without transferring the body.
200 OK: Server sends a new response. Rezo replaces the cached entry.
getConditionalHeaders()
Returns the conditional headers to send for revalidation, based on the cached response:
const headers = cache.getConditionalHeaders('GET', 'https://api.example.com/data');
// { 'If-None-Match': '"abc123"', 'If-Modified-Since': 'Thu, 01 Jan 2026 00:00:00 GMT' }
// or undefined if no cached entry exists updateRevalidated()
When a 304 response is received, updates the cached entry with new headers and refreshes the TTL:
// After receiving a 304 response:
const updated = cache.updateRevalidated('GET', url, newHeaders);
// Returns the updated CachedResponse with refreshed timestamp and TTL If the 304 response includes Cache-Control: no-store, the cached entry is deleted instead.
Cache-Control Compliance
When respectHeaders: true (the default), the cache parses and respects Cache-Control directives:
no-store
Response is never cached. If already cached, it is removed.
Cache-Control: no-store no-cache
Response is stored but must be revalidated with the server before use. The cache stores the entry but marks it for conditional GET on subsequent requests.
Cache-Control: no-cache must-revalidate
Once the cached entry expires, it must be revalidated before use. Stale entries cannot be served.
Cache-Control: must-revalidate max-age
Sets the TTL for the cached entry, overriding the configured default TTL.
Cache-Control: max-age=3600 -> cache for 1 hour (3,600,000ms) s-maxage
Like max-age but for shared caches. When present, it takes priority over max-age.
Cache-Control: s-maxage=7200 -> cache for 2 hours Cache Key Generation
Cache keys are generated from the HTTP method, URL, and select request headers:
GET:https://api.example.com/data:accept=application/json:encoding=gzip The key includes:
- HTTP method (uppercased)
- Full URL
Acceptheader (if present)Accept-Encodingheader (if present)
This ensures different content negotiations produce different cache entries.
Invalidation
invalidate(url, method?)
Remove cached entries for a specific URL. If method is omitted, all cached methods for that URL are invalidated.
// Invalidate GET cache for a specific URL
client.invalidateCache('https://api.example.com/users');
// Invalidate only HEAD cache
client.invalidateCache('https://api.example.com/users', 'HEAD'); Also removes the corresponding disk file when persistence is enabled.
clear()
Remove all cached entries from memory and disk.
client.clearCache(); Cache Statistics
const stats = client.getCacheStats();
// {
// response: { size: 42, enabled: true, persistent: true },
// dns: { size: 15, enabled: true }
// }
// Direct access
console.log(client.responseCache?.size); // 42
console.log(client.responseCache?.isEnabled); // true
console.log(client.responseCache?.isPersistent); // true The beforeCache Hook
The beforeCache hook fires before a response is stored in the cache. Return false to prevent caching.
const client = new Rezo({
cache: true,
hooks: {
beforeCache: [
(event) => {
// Never cache user-specific data
if (event.url.includes('/me') || event.url.includes('/profile')) {
return false;
}
// Never cache if server says private
if (event.cacheControl?.private) {
return false;
}
// Never cache error-like responses wrapped in 200
// (isCacheable already filters non-2xx)
}
]
}
}); CacheEvent
interface CacheEvent {
/** HTTP status code */
status: number;
/** Response headers */
headers: RezoHeaders;
/** URL being cached */
url: string;
/** Cache key */
cacheKey?: string;
/** Whether the response is cacheable by default rules */
isCacheable: boolean;
/** Parsed Cache-Control directives */
cacheControl?: {
maxAge?: number;
sMaxAge?: number;
noCache?: boolean;
noStore?: boolean;
mustRevalidate?: boolean;
private?: boolean;
public?: boolean;
};
} Cached Response Structure
interface CachedResponse {
status: number;
statusText: string;
headers: Record<string, string>;
data: unknown;
url: string;
timestamp: number;
ttl: number;
etag?: string;
lastModified?: string;
} Practical Examples
API Gateway with Caching
const api = new Rezo({
baseURL: 'https://api.example.com',
cache: {
response: {
enable: true,
ttl: 300000, // 5 minutes
maxEntries: 1000,
respectHeaders: true // Server can override TTL
}
}
});
// Cached automatically
const users = await api.get('/users');
// Invalidate after mutation
await api.post('/users', { name: 'Alice' });
api.invalidateCache('https://api.example.com/users'); Persistent Cache for CLI Tool
import { join } from 'node:path';
import { homedir } from 'node:os';
const client = new Rezo({
cache: {
response: {
enable: true,
cacheDir: join(homedir(), '.myapp', 'http-cache'),
ttl: 86400000, // 24 hours
maxEntries: 5000
}
}
});
// Cached responses survive process restarts
const { data } = await client.get('https://registry.npmjs.org/rezo'); Selective Caching with Hooks
const client = new Rezo({
cache: { response: { enable: true, ttl: 600000 } },
hooks: {
beforeCache: [
(event) => {
// Only cache responses with explicit cache headers
if (!event.cacheControl?.maxAge && !event.headers.get('etag')) {
return false;
}
}
]
}
}); Cache with Revalidation
const client = new Rezo({
cache: {
response: {
enable: true,
ttl: 60000, // 1 minute in-memory TTL
respectHeaders: true // But server's max-age takes priority
}
}
});
// First request: full response, cached with ETag
await client.get('https://api.example.com/config');
// After TTL expires: conditional GET with If-None-Match
// If 304: cache refreshed, no body transferred
// If 200: new data cached
await client.get('https://api.example.com/config');