Caching
Improve performance with built-in in-memory and persistent caching. Reduce network calls, speed up your UI, and control cache invalidation.
Caller provides a layered caching system that works out of the box with in-memory storage and can be extended to persistent stores like Redis.
How Caching Works
When you call .stale(milliseconds) on a request, Caller:
- Checks if a cached response exists and is within the stale window
- If found and fresh → returns cached data immediately (no network call)
- If not found or expired → makes the network request → caches the result
// First call — network request, cached for 30s
const { data: fresh } = await api.get('/users')
.stale(30_000)
.execute();
// Second call (within 30s) — instant cached response
const { data: cached } = await api.get('/users')
.stale(30_000)
.execute();
// No network request made!Basic Caching
The .stale() method accepts milliseconds. Simple and predictable:
// Cache for 30 seconds
await api.get('/users').stale(30_000).execute();
// Cache for 5 minutes
await api.get('/config').stale(300_000).execute();
// Cache for 1 hour
await api.get('/constants').stale(3_600_000).execute();Cache Keys
By default, Caller generates cache keys from the request URL. Use .cache() to set a custom key:
// Custom cache key for manual invalidation
await api.get('/me')
.cache('default', 'current-user')
.stale(60_000)
.execute();
// Same data from a different endpoint can share the cache key
await api.get('/users/current')
.cache('default', 'current-user')
.stale(60_000)
.execute();Fetch Cache Strategy
The first parameter to .cache() is the native RequestCache strategy:
| Strategy | Behavior |
|---|---|
'default' | Browser default cache behavior |
'no-store' | Never cache (bypasses both browser and Caller cache) |
'reload' | Fetch from network, update cache |
'force-cache' | Use cache even if stale |
'only-if-cached' | Only return cached data, error otherwise |
// Force network fetch, bypassing browser cache
await api.get('/real-time-data')
.cache('no-store')
.execute();
// Prefer cache for static data
await api.get('/static-content')
.cache('force-cache')
.stale(600_000)
.execute();Cache Invalidation
Invalidate cached entries when data changes (e.g., after a mutation):
Programmatic Invalidation
Use the static IgniterCallerManager methods:
import { IgniterCallerManager } from '@igniter-js/caller';
// After creating a user, invalidate the users list
const { error } = await api.post('/users').body(newUser).execute();
if (!error) {
IgniterCallerManager.on('response', (result, context) => {
// React to specific responses
if (context.url.includes('/users') && context.method === 'POST') {
// Clear cache for this resource
console.log('User created — cache should be refreshed');
}
});
}React Hook Invalidation
In React, use the .invalidate() method from hooks:
function CreateUserForm() {
const main = useCaller('main');
const handleSubmit = async (data: CreateUserInput) => {
const { error } = await main.post('/users').body(data).execute();
if (!error) {
// Invalidate all cached queries for /users
main.invalidate('/users');
}
};
return <form onSubmit={handleSubmit}>...</form>;
}Persistent Caching (Redis / Store Adapter)
For server-side applications, use a persistent store to share cache across instances:
import { IgniterCaller } from '@igniter-js/caller';
// Implement the StoreAdapter interface for your backend
class RedisStoreAdapter {
readonly client: RedisClient;
constructor(redisUrl: string) {
this.client = createRedisClient(redisUrl);
}
async get<T>(key: string): Promise<T | null> {
const raw = await this.client.get(key);
return raw ? JSON.parse(raw) : null;
}
async set(key: string, value: any, options?: { ttl?: number }): Promise<void> {
const serialized = JSON.stringify(value);
if (options?.ttl) {
await this.client.setex(key, options.ttl, serialized);
} else {
await this.client.set(key, serialized);
}
}
async delete(key: string): Promise<void> {
await this.client.del(key);
}
async has(key: string): Promise<boolean> {
return (await this.client.exists(key)) === 1;
}
}
// Use it
const redisStore = new RedisStoreAdapter(process.env.REDIS_URL!);
const api = IgniterCaller.create()
.withBaseUrl('https://api.example.com')
.withStore(redisStore, {
ttl: 3600, // 1 hour default TTL
keyPrefix: 'api-cache:', // Redis key prefix
fallbackToFetch: true, // Fall back to in-memory if Redis is down
})
.build();Store Options
| Option | Default | Description |
|---|---|---|
ttl | 3600 | Default TTL in seconds |
keyPrefix | 'igniter:caller:' | Prefix for all cache keys |
fallbackToFetch | true | Fall back to in-memory when store is unavailable |
Testing with MockStoreAdapter
Use the built-in mock adapter for testing:
import { MockCallerStoreAdapter } from '@igniter-js/caller/adapters';
import { IgniterCaller } from '@igniter-js/caller';
describe('Cache behavior', () => {
it('uses store for caching', async () => {
const store = MockCallerStoreAdapter.create();
const api = IgniterCaller.create()
.withStore(store)
.build();
// First call: cache miss
await api.get('/data').stale(30_000).execute();
// Second call: should hit cache
await api.get('/data').stale(30_000).execute();
// Assert
expect(store.calls.set).toBe(1);
expect(store.calls.get).toBeGreaterThanOrEqual(1);
});
});Caching with Telemetry
Monitor cache performance with telemetry events:
import { IgniterCallerTelemetryEvents } from '@igniter-js/caller/telemetry';
// Every cache hit emits: igniter.caller.cache.read.hit
// Every successful request emits: igniter.caller.request.execute.success (with ctx.cache.hit)Best Practices
Caching Guidelines
- Cache GET requests — mutations (POST/PUT/PATCH/DELETE) always bypass cache
- Use reasonable stale times — 5-30s for dynamic data, 5-60min for static data
- Invalidate on mutation — clear related caches when data changes
- Use custom keys for data that doesn't map 1:1 to URLs
- Monitor hit rates with telemetry to tune stale times
Common Mistakes
- Don't cache user-specific data with shared keys — use user-specific cache keys
- Don't set extremely long stale times for rapidly changing data
- Don't forget that cache is in-memory by default — restarts clear it
- Don't use
.stale()without a store adapter if you need cross-instance caching
Next Steps
HTTP Methods & Response Parsing
Deep dive into HTTP method handling, automatic content-type detection, and response parsing strategies. Learn how Caller processes different data formats.
Interceptors
Transform requests and responses globally with interceptors. Perfect for authentication, logging, error normalization, and request modification.