Caching API calls for better performance

Caching API calls for better performance

Caching API calls is one of the most important aspects of a life of a frontend developer. And as you grow in your career, you must know some caching techniques to make your website more performant.

Here, I am going to explain one very simple way to add a basic caching while making API calls. The main concept I am going to use here is "JavaScript Closures".

Detour - Understanding Closures

When we see or listen closures, we think of it as something very complicated, but it actually is very simple. It just returns another function which has the context of the parent function even after the parent function has stopped executing. For example:

const parentFn = (firstName, lastName) => {
    // concatinating firstName and lastName to form fullName
    let fullName = `${firstName} ${lastName}`;

    // returning an anonymous function
    return () => {
        // printing the `fullName` variable from parent function

// executing the parentFn and storing the response in a variable `getFullName`
// since parentFn returns a function, `getFullName` will be a function
const getFullName = parentFn('Shubhaw', 'Kumar');

// executing the `getFullName` function
getFullName(); // prints "Shubhaw Kumar"

In the above example, the parent function returns a child function. Now, when this child function getFullName is called, it prints the value of fullName variable. But notice that the fullName was a variable of the parentFn which has already finished executing. And we know when a function stops executing, its variables are also thrown out of the execution context. But since the parent function returns a child function, the child function still keeps the variables of parent function in its own execution context and thus, can access those variables. Hence, in our example, the child function is able to successfully print the correct value of fullName variable.

Back to the main topic

Now, using the same concept we discussed above, we are going to implement a simple caching mechanism in our code below. First let's look at the code, and then I'll give the explanation:

// Implementing the caching function
const cacheWrapper = (timeInMs) => {
    let cache = {}; // initializing with an empty object

    return async (url, config) => {
        // key to identify a unique combination of url & config 
        const key = `${url}_${JSON.stringify(config)}`;

        // fetching the entry for the above key from the cache
        const entry = cache[key];

        // if entry is not present, or the existing entry has expired, make the API again
        if (!entry || Date.Now() > entry.expiryTime) {
            // using try-catch is a good practice to make sure your UX is not breaking            
            try {
                console.log("Fetching new data");
                // calling the API again
                let response = await fetch(url, config);
                response = await response.json();
                // storing the result in the cache object for the `key`
                cache[key] = {
                    data: response,
                    expiryTime: Date.Now() + timeInMs,
            } catch (error) {
                // either log the error on the console
                // or handle it gracefully like showing a pop-up, etc.
                console.error("Error fetching data");

        // return the result from the cache
        return cache[key];

// calling the cacheWrapper to get cached API caller
const fetchWithCache = cacheWrapper(5000);

// fetching the API result
fetchWithCache("", {
    method: "GET",
}).then((data) => console.log("Without timeout:", data));

// fetching the response within 1 sec
// this will return the cached result
setTimeout(() => {
    fetchWithCache("", {
        method: "GET",
    }).then((data) => console.log("With 1 sec timeout:", data));
}, 1000);

The above code is very similar to the one I explained in the closure example. Instead of the parentFn , the name here is cacheWrapper. Instead of the fullName variable, we are using cache variable which is of object type and instead of the 1 liner anonymous function, we are using a little complex async function here.

This cacheWrapper function accepts timeInMs argument which is used to invalidate the cache, i.e., once the given amount of time passes, we should no more return the cached result, instead we should make an actual call to the API. This is a necessary practice so that there's we do not end of showing any stale data.

In this async function, we first define a key using the url and config provided to us. This is our unique identifier based on which we can distinguish between the different API calls. Using this key we update the cache object. You are free to choose any object structure, but, for simplicity, I have just kept two properties in the object - the api response data and the expiryTime. This expiryTime uses the timeInMs argument's value to calculate the time after which the particular cache value should be invalidated.

Now, based on this key, we try to check if there's an entry for the key already in the cache object or not. If not, this means for the particular combination of url and config no API call has been made yet. So, we make the API call and put the response in the cache and return the data back as well. And, we do the exact same thing in cases where an entry is there for a particular key but the time has expired, so we again make another API call and replace the older cache value with the new one. As mentioned earlier, this makes sure that our data has never gone stale.

Now, you can get a cached API caller by executing the cacheWrapper . Here, I have called it as fetchWithCache . Using this fetchWithCache you can make the API calls, frequently and it will make the actual API calls only when required.

That's all about it. Let me know if you have any kind of questions regarding this either here or over my LinkedIn.

Did you find this article valuable?

Support Shubhaw Kumar by becoming a sponsor. Any amount is appreciated!