JAVASCRIPT

JavaScript: Simple API Request with Caching and Stale-While-Revalidate

Implement client-side caching for API requests with a 'stale-while-revalidate' strategy, improving perceived performance and reducing unnecessary network calls.

const cache = new Map();

async function fetchDataWithCaching(url, options = {}) {
  const cacheKey = JSON.stringify({ url, options });
  const cacheEntry = cache.get(cacheKey);
  const now = Date.now();

  // Define cache validity periods (in milliseconds)
  const STALE_AFTER = 5 * 60 * 1000; // 5 minutes
  const EXPIRE_AFTER = 60 * 60 * 1000; // 60 minutes

  if (cacheEntry) {
    const { data, timestamp } = cacheEntry;
    
    // Serve stale data immediately if within EXPIRE_AFTER
    if (now - timestamp < EXPIRE_AFTER) {
      console.log(`[Cache] Serving ${url} from cache.`);
      // If data is stale, revalidate in background
      if (now - timestamp > STALE_AFTER && !cacheEntry.revalidating) {
        console.log(`[Cache] ${url} is stale, revalidating in background.`);
        cacheEntry.revalidating = true;
        fetch(url, options)
          .then(response => response.json())
          .then(newData => {
            cache.set(cacheKey, { data: newData, timestamp: Date.now(), revalidating: false });
            console.log(`[Cache] ${url} revalidated successfully.`);
          })
          .catch(error => console.error(`[Cache] Background revalidation failed for ${url}:`, error))
          .finally(() => cacheEntry.revalidating = false);
      }
      return data;
    } else {
      // Cache expired, remove it
      cache.delete(cacheKey);
      console.log(`[Cache] ${url} cache expired.`);
    }
  }

  // No cache entry or expired, fetch new data
  console.log(`[Cache] Fetching fresh data for ${url}.`);
  try {
    const response = await fetch(url, options);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const freshData = await response.json();
    cache.set(cacheKey, { data: freshData, timestamp: Date.now(), revalidating: false });
    return freshData;
  } catch (error) {
    console.error(`Error fetching data for ${url}:`, error);
    throw error;
  }
}

// Example Usage:
async function demoCaching() {
  const apiEndpoint = 'https://jsonplaceholder.typicode.com/posts/1';

  console.log('
--- First Request (should fetch fresh) ---');
  let data1 = await fetchDataWithCaching(apiEndpoint);
  console.log('Data 1:', data1.title);

  console.log('
--- Second Request (should be from cache) ---');
  let data2 = await fetchDataWithCaching(apiEndpoint);
  console.log('Data 2:', data2.title);

  // Simulate passing the stale-after time
  console.log('
--- Simulating time passing (5.5 minutes for stale) ---');
  // For demonstration only: Manually adjust timestamp to simulate staleness
  const cacheKey = JSON.stringify({ url: apiEndpoint, options: {} });
  const entry = cache.get(cacheKey);
  if (entry) {
    entry.timestamp = Date.now() - (5.5 * 60 * 1000); // Make it stale
  }

  console.log('
--- Third Request (should return stale, revalidate in background) ---');
  let data3 = await fetchDataWithCaching(apiEndpoint);
  console.log('Data 3 (stale):', data3.title);

  console.log('
--- Wait for background revalidation... (2 seconds) ---');
  await new Promise(resolve => setTimeout(resolve, 2000));

  console.log('
--- Fourth Request (should be fresh from background revalidation) ---');
  let data4 = await fetchDataWithCaching(apiEndpoint);
  console.log('Data 4 (fresh):', data4.title);
}

demoCaching();
How it works: This JavaScript snippet implements a client-side caching strategy for API requests, specifically using the "stale-while-revalidate" pattern. When data is requested, it first checks a local `Map` cache. If a valid, non-expired entry exists, it's returned immediately to improve perceived performance. If the cached data is 'stale' (older than `STALE_AFTER` duration) but not yet 'expired' (older than `EXPIRE_AFTER`), the stale data is returned, and a fresh data fetch is initiated in the background to update the cache. If no valid cache entry exists, new data is fetched from the API and then stored in the cache. This approach balances responsiveness with data freshness, ensuring users see content quickly while data updates asynchronously.

Need help integrating this into your project?

Our team of expert developers can help you build your custom application from scratch.

Hire DigitalCodeLabs