I created a Node JS project recently that required fetching some data from an external API.
A colleague suggested that I cache the API call response. I thought this suggestion was a good idea since the data did not change very often and the app ran continuously hitting the same endpoint frequently. A cache would cut down on network requests and boost performance, since fetching from memory is typically much faster than making an API request.
I ended up creating a simple in-memory cache and made it reusable, so I can repurpose it for other projects.
The app made a network request that looked something like this, using the Axios library:
const axios = require('axios');const getUnemploymentRate = () => {const url = 'https://api.bls.gov/publicAPI/v2/timeseries/data/LNS14000000';return axios.get(url).then((result) => result.data);}
The function retrieves the most recent U.S. unemployment figures from the U.S. Bureau of Labor Statistics. (The BLS API interface is here: https://www.bls.gov/developers/api_signature_v2.htm)
It returns a Promise that resolves to an object containing the unemployment rates for each month going back about two years.
Here's a sample use-case:
getUnemploymentRate().then((result) => {// do some stuff with result};
Output snippet:
{"status":"REQUEST_SUCCEEDED","responseTime":119,"message":[],"Results":{"series":[{"seriesID":"LNS14000000","data":[{"year":"2018","period":"M10","periodName":"October","latest":"true","value":"3.7","footnotes":[{}]},{"year":"2018","period":"M09","periodName":"September","value":"3.7","footnotes":[{}]},{"year":"2018","period":"M08","periodName":"August","value":"3.9","footnotes":[{}]},{"year":"2018","period":"M07","periodName":"July","value":"3.9","footnotes":[{}]},{"year":"2018","period":"M06","periodName":"June","value":"4.0","footnotes":[{}]},...
This request is a perfect candidate for caching since the unemployment data changes only once a month.
In developing the cache, I had a few objectives:
- Make it a simple, in-memory storage cache
- Make it return a JavaScript Promise regardless of serving fresh or cached data
- Make it reusable for other types of data, not just this particular data set
- Make the cache life, or "time-to-live" (TTL) configurable
The resulting JavaScript class has a constructor with two parameters:
The class has four properties: the
The class's three methods are:
class DataCache {constructor(fetchFunction, minutesToLive = 10) {this.millisecondsToLive = minutesToLive * 60 * 1000;this.fetchFunction = fetchFunction;this.cache = null;this.getData = this.getData.bind(this);this.resetCache = this.resetCache.bind(this);this.isCacheExpired = this.isCacheExpired.bind(this);this.fetchDate = new Date(0);}isCacheExpired() {return (this.fetchDate.getTime() + this.millisecondsToLive) < new Date().getTime();}getData() {if (!this.cache || this.isCacheExpired()) {console.log('expired - fetching new data');return this.fetchFunction().then((data) => {this.cache = data;this.fetchDate = new Date();return data;});} else {console.log('cache hit');return Promise.resolve(this.cache);}}resetCache() {this.fetchDate = new Date(0);}}
(I included console.logs so I could test to make sure the cache was working properly. You can see the results of running the cache below. I removed the log statements in the final code.)
To use the cache instead of calling the API directly every time, create a new instance of DataCache, passing in the original data fetch function as the callback function argument.
const unemploymentRateCache = new DataCache(getUnemploymentRate);
That cache instance can then be used in this way:
unemploymentRateCache.getData().then((result) => {// do some stuff with result});
To test, I created a new instance of the DataCache, but passed in a short cache life so it will expire in just a few seconds.
const unemploymentRateCache = new DataCache(getUnemploymentRate, .05);
.05 minutes will give the cache a time-to-live of about 3 seconds. Then several setTimeouts triggered the data fetching every second:
setTimeout(unemploymentRateCache.getData, 0);setTimeout(unemploymentRateCache.getData, 1000);setTimeout(unemploymentRateCache.getData, 2000);setTimeout(unemploymentRateCache.getData, 3000);setTimeout(unemploymentRateCache.getData, 4000);setTimeout(unemploymentRateCache.getData, 5000);setTimeout(unemploymentRateCache.getData, 6000);
The result:
$ node index.jsexpired - fetching new datacache hitcache hitcache hitexpired - fetching new datacache hitcache hit
This solution is obviously not the best one for all use cases. Since the cache is stored in memory, it doesn't persist if the app crashes or if the server is restarted. And if it's used to store a relatively large amount of data, it could have a negative impact on your app's performance.
But if you have an app that makes relatively small data requests to the same endpoint numerous times, this might work well for your purposes.
Postscript: While working on this blog post, I ran up against a rate limiter on the BLS API. You can probably avoid that by signing up for a free API registration key and passing it along with your parameters as described in the docs linked to above. Register here: https://data.bls.gov/registrationEngine/