All posts

Create a simple API caching layer using Typescript

Caching API requests is something that's often forgotten, even though it can possibly improve performance and reduce use of resources. The implementation can be really simple, as I will show you.

For this method I'm using memory caching. That means the cache will only live in the browser tab as long as the end user does not refresh. Thus this is especially useful for single page applications, where an end user almost never uses page refreshes. If you're building a website that does make use of page refreshes, you could consider adding some persistance layer that writes your cache to localStorage or sessionStorage for example.

In the following example I've also chosen to make the ApiCache class a singleton. A pattern you don't see much in the JS/TS world, but I find it pretty convenient. A singleton makes sure you only have a single instance of your class. In this case it's useful to make sure we only have one cache where every request is saved. You can still use new ApiCache(); everywhere, it just refers to the single instance.

This caching layer provides with 3 public methods:

Easy as that!

type CacheResult = any;

export class ApiCache {
    private static instance: ApiCache;
    private cache: { url: string; body: string, result: CacheResult }[] = [];

    constructor() {
        if (!!ApiCache.instance) {
            return ApiCache.instance;
        }

        ApiCache.instance = this;

        return this;
    }

    public set = (url: string, result: any, body?: string): void => {
        if (!this.recordExists(url, body)) {
            this.cache.push({
                url,
                body,
                result
            });
        }
    }

    public get = (url: string, body?: string): CacheResult | null => {
        const cacheRecord = this.cache.find(x => {
            if (body) {
                return x.url === url && x.body === body;
            }

            return x.url === url;
        });

        if (cacheRecord) {
            return cacheRecord.result;
        }

        return null;
    }
    
    public recordExists = (url: string, body?: string): boolean => {
        return !!this.get(url, body);
    }
}

The implementation will look something like this:

export class Api {
    private apiCache = new ApiCache();

    public getResults = async () => {
        return this.getData<any>(`/url-for-results`);
    }

    private async getData<ReturnT>(url: string): Promise<ReturnT> {
        if (this.apiCache.recordExists(url)) {
            return new Promise(resolve => 
                resolve(this.apiCache.get(url))
            );
        }

        try {
            return await fetch(url)
                .then(async (res) => {
                    if (!res.ok) {
                        throw (res);
                    }

                    const text = await res.text();
                    const json = JSON.parse(text);

                    this.apiCache.set(url, json);

                    return json;
                });
        } catch (e) {
            console.error(`API call '${url}' fails with code: ${e.statusCode}. Exception: ${e.toString()}`);
        }
    }
}

Are you using another way to cache your responses? Having some comments about this implementation? Let me know!

Published Jul 20, 2019

ReactTypescriptcaching
Dev stuff by Marc Veens

Marc Veens

Stories about my adventures in the wondrous world of front-end development.