mentation demonstrates a production-ready approach that encapsulates cache mode selection, respects server directives, and provides clear architectural boundaries.
Step 1: Define a Typed Request Configuration
TypeScript interfaces enforce consistency and prevent runtime misconfiguration. We separate cache semantics from network transport concerns.
type CacheDirective = 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached';
interface RequestCacheConfig {
endpoint: string;
cacheMode: CacheDirective;
headers?: Record<string, string>;
timeoutMs?: number;
requiresSameOrigin?: boolean;
}
Step 2: Build a Cache-Aware Fetch Factory
The factory function applies the cache directive, enforces safety constraints, and handles timeout boundaries. It explicitly documents why each architectural choice exists.
async function executeCachedRequest(config: RequestCacheConfig): Promise<Response> {
const { endpoint, cacheMode, headers = {}, timeoutMs = 10000, requiresSameOrigin = false } = config;
// Architectural rationale: only-if-cached strictly requires same-origin policy.
// Cross-origin requests will throw a TypeError if mode is not constrained.
const requestMode = requiresSameOrigin || cacheMode === 'only-if-cached' ? 'same-origin' : 'cors';
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(endpoint, {
method: 'GET',
cache: cacheMode,
mode: requestMode,
headers: {
'Accept': 'application/json',
...headers,
},
signal: controller.signal,
});
return response;
} finally {
clearTimeout(timeoutId);
}
}
Step 3: Apply Modes to Real-World Scenarios
Each cache mode serves a distinct data lifecycle. The following examples demonstrate correct usage patterns with equivalent functionality to standard use cases, but structured for production maintainability.
Standard Data Retrieval (default)
// Relies on server Cache-Control and ETag headers.
// Browser returns fresh cache, validates stale entries via 304, or fetches new.
const dashboardMetrics = await executeCachedRequest({
endpoint: '/api/v1/metrics/overview',
cacheMode: 'default',
});
Real-Time Telemetry (no-store)
// Bypasses cache entirely. Guarantees network freshness.
// Ideal for auction bids, live sensor streams, or financial tickers.
const liveOrderBook = await executeCachedRequest({
endpoint: '/api/v1/trading/orderbook',
cacheMode: 'no-store',
});
Explicit Refresh (reload)
// Skips existing cache on request, stores result for subsequent calls.
// Use after user-triggered sync or post-mutation state reconciliation.
const updatedUserProfile = await executeCachedRequest({
endpoint: '/api/v1/users/me',
cacheMode: 'reload',
});
Conditional Validation (no-cache)
// Always sends If-None-Match / If-Modified-Since.
// Server responds 304 if unchanged, full payload if modified.
const featureToggles = await executeCachedRequest({
endpoint: '/api/v1/config/flags',
cacheMode: 'no-cache',
});
Static Reference Data (force-cache)
// Prefers cached response regardless of staleness.
// Only contacts network on complete cache miss.
const currencyRegistry = await executeCachedRequest({
endpoint: '/api/v1/reference/currencies',
cacheMode: 'force-cache',
});
Offline-First Routing (only-if-cached)
// Never initiates network traffic. Throws on cache miss.
// Must pair with same-origin mode.
const cachedArticle = await executeCachedRequest({
endpoint: '/api/v1/content/article/8842',
cacheMode: 'only-if-cached',
requiresSameOrigin: true,
});
Architecture Decisions & Rationale
- Separation of Cache Semantics from Transport: The
cache option influences browser behavior but does not override server Cache-Control, ETag, or Last-Modified headers. The factory respects this boundary by treating cache modes as client-side preferences rather than absolute commands.
- AbortController Integration: Network requests in production require timeout boundaries. Wrapping
fetch with an abort signal prevents zombie requests from blocking UI threads or consuming memory.
- Mode Enforcement for
only-if-cached: The Fetch specification mandates same-origin for this mode. The factory automatically applies the constraint, preventing silent failures in cross-origin environments.
- Header Normalization: Standardizing
Accept and merging custom headers ensures consistent content negotiation, which improves cache key accuracy across different endpoints.
Pitfall Guide
1. Misinterpreting no-cache as "Disable Caching"
Explanation: The name is historically misleading. no-cache does not prevent storage; it forces validation before reuse. The browser will still store the response and reuse it if the server returns 304 Not Modified.
Fix: Use no-store when you absolutely must prevent local persistence. Reserve no-cache for resources that change unpredictably but benefit from conditional validation.
Explanation: The cache option is advisory. If a server responds with Cache-Control: no-store, the browser will ignore force-cache or default and refuse to store the response.
Fix: Align client cache modes with server directives. Audit response headers in DevTools Network tab. If server headers conflict with client intent, negotiate header adjustments with the backend team.
3. Using only-if-cached with Cross-Origin Endpoints
Explanation: The specification explicitly blocks cross-origin requests when only-if-cached is active. The fetch will reject with a network error, often misdiagnosed as a CORS issue.
Fix: Always set mode: 'same-origin' when using only-if-cached. For cross-origin offline strategies, implement service worker interception with the Cache API instead of relying on fetch cache modes.
4. Confusing HTTP Cache with In-Memory Application State
Explanation: Frameworks like React Query or SWR maintain their own caches in JavaScript memory. The Fetch API cache option operates at the browser HTTP layer. They do not automatically synchronize.
Fix: Treat HTTP cache as a transport optimization and application cache as a UI state manager. Disable framework caching when using no-store or reload to prevent stale UI states from persisting after network bypass.
5. Overusing force-cache for Dynamic Endpoints
Explanation: force-cache serves stale data without revalidation. Applying it to user-specific or frequently updated endpoints causes persistent UI inconsistencies.
Fix: Restrict force-cache to immutable reference data (country codes, timezone tables, static assets). Use no-cache or default for any endpoint that changes based on user context or time.
6. Ignoring Cache Key Generation Nuances
Explanation: The browser caches based on the full request URL, including query parameters. Two requests to /api/data?page=1 and /api/data?page=2 are cached separately. Developers often assume cache modes apply globally across parameterized routes.
Fix: Design endpoints with cache-friendly URLs. If parameterized requests should share cache entries, normalize parameters or use service worker route matching to unify cache keys.
7. Forgetting to Handle Cache Miss Errors Gracefully
Explanation: only-if-cached throws on miss. force-cache may return stale data without warning. Applications that don't explicitly handle these states degrade silently.
Fix: Wrap cache-reliant fetches in try/catch blocks. Implement fallback UI states for cache misses. Log cache hit/miss ratios to monitor data freshness in production.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Real-time financial data | no-store | Guarantees network freshness, prevents stale pricing | Higher bandwidth, lower server cache efficiency |
| User preference sync | no-cache | Validates via 304, reduces payload when unchanged | Moderate network overhead, high cache efficiency |
| Static reference tables | force-cache | Eliminates network calls for immutable data | Near-zero bandwidth, minimal server load |
| Post-form submission refresh | reload | Bypasses stale cache, stores fresh response for reuse | One-time network cost, improved subsequent performance |
| Offline-first content reader | only-if-cached + fallback | Prevents accidental network calls, enables graceful degradation | Zero network cost when cached, requires error handling |
Configuration Template
// network/cache-manager.ts
export type CacheMode = 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached';
export interface CacheRequestOptions {
url: string;
mode: CacheMode;
headers?: HeadersInit;
timeout?: number;
sameOriginOnly?: boolean;
}
export async function cachedFetch(options: CacheRequestOptions): Promise<Response> {
const { url, mode, headers = {}, timeout = 8000, sameOriginOnly = false } = options;
const effectiveMode = sameOriginOnly || mode === 'only-if-cached' ? 'same-origin' : 'cors';
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
return await fetch(url, {
method: 'GET',
cache: mode,
mode: effectiveMode,
headers: { 'Accept': 'application/json', ...headers },
signal: controller.signal,
});
} finally {
clearTimeout(timer);
}
}
// Usage example
const response = await cachedFetch({
url: '/api/v1/inventory/status',
mode: 'no-cache',
timeout: 5000,
});
Quick Start Guide
- Identify Data Volatility: Classify your endpoints into three tiers: immutable (reference data), semi-dynamic (user settings, feature flags), and real-time (live feeds, transactions).
- Map Cache Modes: Assign
force-cache to immutable, no-cache or default to semi-dynamic, and no-store to real-time endpoints.
- Replace Raw Fetch Calls: Swap direct
fetch() invocations with the provided cachedFetch template, passing the appropriate mode per endpoint.
- Validate in DevTools: Open the Network panel, enable "Disable cache" to test fresh fetches, then disable it to verify
304 responses and cache storage behavior.
- Implement Fallbacks: Wrap
only-if-cached and force-cache calls in error boundaries or loading states to handle cache misses and stale data gracefully.