frontend
Caching API Calls in JavaScript
January 24, 2026
Caching API Calls in JavaScript
Overview
Caching API calls is a technique to store API responses temporarily to avoid redundant network requests. This improves performance, reduces server load, and provides better user experience by serving cached data when available and valid.
Basic Implementation
/** Caching API call */
let cache = {};
function cacheMethod(url) {
// Cache the url and check if expiry date is passed or not
if (cache[url] && cache[url].expiresAt > Date.now()) {
return Promise.resolve(cache[url].data);
}
// If cache is not present then make a new API call
return fetch(url)
.then((res) => res.json())
.then((res) => {
// Cache duration
const cacheDuration = 60 * 1000; // 1 minute
const expiresAt = Date.now() + cacheDuration;
const data = res.products;
// Pass the data to the cache
cache[url] = { data, expiresAt };
return data;
});
}
// Usage
cacheMethod("https://dummyjson.com/products?limit=10").then((d) => {
console.log("d==>", d);
// Second call is not made in network tab, only promise is being returned
cacheMethod("https://dummyjson.com/products?limit=10").then((a) => {
console.log("a==>", a);
});
});
Advanced Implementation
Class-Based API Cache
class APICache {
constructor(options = {}) {
this.cache = new Map();
this.defaultTTL = options.defaultTTL || 60000; // 1 minute
this.maxSize = options.maxSize || 100;
}
async get(url, options = {}) {
const {
ttl = this.defaultTTL,
forceRefresh = false,
key = url
} = options;
// Check cache
if (!forceRefresh && this.cache.has(key)) {
const cached = this.cache.get(key);
if (cached.expiresAt > Date.now()) {
return cached.data;
}
// Expired, remove it
this.cache.delete(key);
}
// Fetch and cache
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Store in cache
this.set(key, data, ttl);
return data;
} catch (error) {
console.error('API call failed:', error);
throw error;
}
}
set(key, data, ttl = this.defaultTTL) {
// Remove oldest if at capacity
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
data,
expiresAt: Date.now() + ttl
});
}
has(key) {
if (!this.cache.has(key)) {
return false;
}
const cached = this.cache.get(key);
if (cached.expiresAt <= Date.now()) {
this.cache.delete(key);
return false;
}
return true;
}
delete(key) {
return this.cache.delete(key);
}
clear() {
this.cache.clear();
}
size() {
return this.cache.size;
}
cleanup() {
const now = Date.now();
for (const [key, value] of this.cache.entries()) {
if (value.expiresAt <= now) {
this.cache.delete(key);
}
}
}
}
Use Cases
1. Basic API Caching
const apiCache = new APICache({ defaultTTL: 300000 }); // 5 minutes
async function fetchUserData(userId) {
return apiCache.get(`/api/users/${userId}`);
}
2. Different TTLs for Different Endpoints
async function fetchData(endpoint) {
const ttlMap = {
'/api/users': 60000, // 1 minute
'/api/posts': 300000, // 5 minutes
'/api/settings': 3600000 // 1 hour
};
return apiCache.get(endpoint, {
ttl: ttlMap[endpoint] || 60000
});
}
3. Cache with Request Options
class AdvancedAPICache extends APICache {
async get(url, options = {}) {
const {
method = 'GET',
headers = {},
body = null,
...cacheOptions
} = options;
// Create cache key from URL and options
const cacheKey = JSON.stringify({ url, method, headers, body });
if (!options.forceRefresh && this.has(cacheKey)) {
return this.cache.get(cacheKey).data;
}
const fetchOptions = { method, headers };
if (body) {
fetchOptions.body = JSON.stringify(body);
fetchOptions.headers['Content-Type'] = 'application/json';
}
const response = await fetch(url, fetchOptions);
const data = await response.json();
this.set(cacheKey, data, cacheOptions.ttl);
return data;
}
}
Advanced Features
1. Cache with Stale-While-Revalidate
class StaleWhileRevalidateCache extends APICache {
async get(url, options = {}) {
const key = options.key || url;
const cached = this.cache.get(key);
if (cached) {
const isStale = cached.expiresAt <= Date.now();
if (!isStale) {
return cached.data; // Fresh cache
}
// Stale but return it, then refresh in background
this.refreshInBackground(url, key, options);
return cached.data;
}
// No cache, fetch normally
return this.fetchAndCache(url, key, options);
}
async refreshInBackground(url, key, options) {
try {
const response = await fetch(url);
const data = await response.json();
this.set(key, data, options.ttl);
} catch (error) {
console.error('Background refresh failed:', error);
}
}
}
2. Cache with Invalidation
class CacheWithInvalidation extends APICache {
constructor(options = {}) {
super(options);
this.invalidationPatterns = new Map();
}
invalidate(pattern) {
if (typeof pattern === 'string') {
// Simple string match
for (const key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
}
}
} else if (pattern instanceof RegExp) {
// Regex match
for (const key of this.cache.keys()) {
if (pattern.test(key)) {
this.cache.delete(key);
}
}
} else if (typeof pattern === 'function') {
// Function predicate
for (const key of this.cache.keys()) {
if (pattern(key)) {
this.cache.delete(key);
}
}
}
}
setInvalidationRule(urlPattern, invalidateOn) {
this.invalidationPatterns.set(urlPattern, invalidateOn);
}
}
3. Cache with Statistics
class CacheWithStats extends APICache {
constructor(options = {}) {
super(options);
this.stats = {
hits: 0,
misses: 0,
sets: 0,
errors: 0
};
}
async get(url, options = {}) {
const key = options.key || url;
if (this.has(key)) {
this.stats.hits++;
return this.cache.get(key).data;
}
this.stats.misses++;
try {
const data = await this.fetchAndCache(url, key, options);
this.stats.sets++;
return data;
} catch (error) {
this.stats.errors++;
throw error;
}
}
getStats() {
const total = this.stats.hits + this.stats.misses;
return {
...this.stats,
hitRate: total > 0 ? (this.stats.hits / total * 100).toFixed(2) + '%' : '0%',
size: this.cache.size
};
}
}
React Hook Example
import { useState, useEffect, useRef } from 'react';
function useCachedAPI(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const cacheRef = useRef(new APICache());
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
const result = await cacheRef.current.get(url, options);
if (!cancelled) {
setData(result);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data, loading, error } = useCachedAPI(`/api/users/${userId}`, {
ttl: 300000
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{data.name}</div>;
}
Best Practices
- Set Appropriate TTLs: Balance freshness with performance
- Handle Errors: Don't cache error responses
- Cache Key Strategy: Use consistent cache keys
- Memory Management: Limit cache size and cleanup expired entries
- Invalidation: Provide ways to invalidate cache when needed
- Stale-While-Revalidate: Consider serving stale data while refreshing
Real-World Example
class ProductionAPICache {
constructor() {
this.cache = new Map();
this.defaultTTL = 300000; // 5 minutes
this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
}
async get(url, options = {}) {
const { ttl = this.defaultTTL, forceRefresh = false } = options;
const key = this.createKey(url, options);
if (!forceRefresh && this.cache.has(key)) {
const cached = this.cache.get(key);
if (cached.expiresAt > Date.now()) {
return cached.data;
}
this.cache.delete(key);
}
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
this.set(key, data, ttl);
return data;
} catch (error) {
// Return stale cache on error if available
const stale = this.cache.get(key);
if (stale) {
console.warn('Using stale cache due to error:', error);
return stale.data;
}
throw error;
}
}
createKey(url, options) {
return JSON.stringify({ url, ...options });
}
set(key, data, ttl) {
this.cache.set(key, {
data,
expiresAt: Date.now() + ttl
});
}
cleanup() {
const now = Date.now();
for (const [key, value] of this.cache.entries()) {
if (value.expiresAt <= now) {
this.cache.delete(key);
}
}
}
destroy() {
clearInterval(this.cleanupInterval);
this.cache.clear();
}
}