Implementing Memoization for API Calls in JavaScript



Caching identical API calls, also known as memoization, is a common technique used to optimize the performance of web applications by storing the results of expensive function calls and returning the cached result when the same inputs occur again. This technique can significantly reduce the number of network requests, leading to faster response times and reduced server load.

we'll implement a memoization function that caches the results of API calls based on their URL and parameters.


What is Memoization?

Memoization is an optimization technique used to speed up function calls by caching the results of expensive computations. In the context of API calls, memoization involves storing the response of an API request and returning the cached response for identical requests, rather than making the network call again.

Real Interview Insights

Interviewers might ask you to:

  • Implement a caching mechanism that stores the results of API calls.
  • Ensure that identical API requests return the cached response instead of making a new request.
  • Handle cache invalidation or expiration when necessary.

Implementing the Memoization Function

We'll implement a function memoizeAPI that wraps around a function making API calls (e.g., fetch). This function will cache responses based on the URL and query parameters.

Step 1: Define the memoizeAPI Function

The memoizeAPI function will create a cache object that stores the results of API calls. The cache key will be a combination of the API endpoint and query parameters.

function memoizeAPI(fetchFn) {
  const cache = new Map();
 
  return async function(url, options = {}) {
    const cacheKey = JSON.stringify({ url, options });
    
    if (cache.has(cacheKey)) {
      console.log('Returning cached response for:', url);
      return cache.get(cacheKey);
    }
 
    const response = await fetchFn(url, options);
    const data = await response.json();
    
    cache.set(cacheKey, data);
    
    return data;
  };
}

Explanation:

  • Cache as a Map: We use a Map to store the cache. The keys are strings that uniquely identify each API request based on the URL and options.
  • Cache Key Generation: The cacheKey is generated by stringifying the URL and options, ensuring that requests with different parameters are treated as unique.
  • Check and Return Cache: Before making the API call, the function checks if the cacheKey exists in the cache. If it does, it returns the cached response.
  • Store the Response: If the cache doesn't contain the response, the function makes the API call, stores the response in the cache, and then returns the response.

Step 2: Example Usage

Let's see how we can use memoizeAPI to cache API calls.

const memoizedFetch = memoizeAPI(fetch);
 
async function fetchData() {
  const url = 'https://api.example.com/data';
 
  // First call, API request will be made
  const data1 = await memoizedFetch(url);
  console.log(data1);
 
  // Second call with the same URL, cached response will be returned
  const data2 = await memoizedFetch(url);
  console.log(data2);
}
 
fetchData();

Explanation:

  • First Call: The first time memoizedFetch is called with a specific URL, it will make the actual API request and cache the response.
  • Second Call: The second time memoizedFetch is called with the same URL, it will return the cached response, avoiding a network request.

Handling Cache Invalidation and Expiration

In real-world scenarios, cached data might become outdated or invalid. To handle this, you can enhance the memoizeAPI function to include cache expiration:

function memoizeAPI(fetchFn, ttl = 60000) { // ttl in milliseconds
  const cache = new Map();
 
  return async function(url, options = {}) {
    const cacheKey = JSON.stringify({ url, options });
    const now = Date.now();
 
    if (cache.has(cacheKey)) {
      const { data, timestamp } = cache.get(cacheKey);
      
      if (now - timestamp < ttl) {
        console.log('Returning cached response for:', url);
        return data;
      } else {
        console.log('Cache expired for:', url);
        cache.delete(cacheKey);
      }
    }
 
    const response = await fetchFn(url, options);
    const data = await response.json();
    
    cache.set(cacheKey, { data, timestamp: now });
    
    return data;
  };
}

Explanation:

  • TTL (Time to Live): The function takes an optional ttl parameter, specifying how long the cache is valid.
  • Cache Expiration: If the cached response is older than the specified TTL, it is removed from the cache, and a new API call is made.

Step 3: Example with Expiration

const memoizedFetchWithTTL = memoizeAPI(fetch, 5000); // 5 seconds TTL
 
async function fetchData() {
  const url = 'https://api.example.com/data';
 
  // First call, API request will be made
  const data1 = await memoizedFetchWithTTL(url);
  console.log(data1);
 
  // Wait for 6 seconds
  setTimeout(async () => {
    // Cached response will have expired, new API request will be made
    const data2 = await memoizedFetchWithTTL(url);
    console.log(data2);
  }, 6000);
}
 
fetchData();

Handling Edge Cases

  1. Network Errors: Ensure that network errors are handled gracefully and that failed requests are not cached.
  2. Dynamic Parameters: Handle cases where API calls have dynamic parameters that might affect the caching logic.
  3. Cache Bloating: Monitor the size of the cache to prevent excessive memory usage, especially in long-running applications.

Use Cases for Memoization of API Calls

  1. Reducing Network Load: Memoizing API calls can significantly reduce the number of network requests, especially for frequently accessed data.
  2. Improving Performance: By returning cached responses, applications can reduce latency and improve the user experience.
  3. Rate Limiting: Memoization helps avoid hitting API rate limits by reusing responses for identical requests.