Skip to main content

API Caching with Node.js and Redis What Is an API Cache and Why Should We Use It?

I think everyone should already know what a cache is. It (mostly) temporarily stores data that will be needed again in the near future (e.g. to save computing power or network usage).

An API is an application programming interface to which requests can be sent to obtain data.

So how can we use these two technologies together?

Let us refer directly to the example of what we are about to implement ourselves: In our web app, a user enters data that is then sent to our server. We, on our server, do not process the sent data (a number) ourselves. We only evaluate it.

Therefore, our server sends the data it has received from the client to an API, which in our code example performs a calculation (it doubles the value) and then sends the result back to our server. The server, of course, returns the result to the user.

So far, no problem, right? But what if several users send the same number in a very short time? Since our API only multiplies the numbers by two, this means that if the same number is transmitted several times, the same result will be returned again and again — only that a request is needed again and again.

And that becomes a problem because it costs unnecessary capacity. APIs are often not free to use and you pay per large amount of requests.

In addition, API requests take a certain amount of time, depending on location and capacity, which is negative for the user experience.

This is where Redis comes in. Redis is a very, very fast in-memory data structure store, so it’s perfect for the volatile storage of small and non-complex data as well as very fast retrieval.

This Is How We Do It

  1. The client sends a number to our server.
  2. If the number that the client has sent to our server is already stored in our cache as a key (in principle like an ID since only one original number can belong to each result), then using that key, we load the result that was originally generated by the API and send the result directly to the client — without any unnecessary API request.
  3. If the number from the client is not yet stored in our cache as a key, we send it to the API in our request.
  4. In this case, the client receives the result directly from the API, but we also store the key (the number from the user) and the result coming from the API in the cache.

This way, we avoid unnecessary API requests. And if a value has not yet been cached, there is a first time for everything. The second time, it can be read from the cache.

In order to demonstrate this — and because it should be the case in a real-world example — we set up a cache entry to be deleted automatically after 60 seconds. In a real-world project, we should do this to always guarantee fresh results of the API in the long run.

Let’s Get Practical About This

Installing Redis on your machine

Linux:

macOS:

  • Install brew. In case you do not have it already installed, https://brew.sh/.
  • brew install redis
  • brew services start redis

Windows:

  • Use the Linux subsystem for Windows. Then just install it like on Linux.

Check if Redis is working

Once you have already started the Redis service, you should be able to communicate with it via the redis-cli. Try to do redis-cli pingand Redis should answer withPONG. That means Redis is working.

Getting started with Redis in Node.js

First, we need to install Redis in our Node.js project:

npm install redis

Then we can import it in our code and connect to the Redis instance running on our machine. The parameter for the createClient will be the address of our instance on our machine (that’s why we use localhost) and the port 6379 is the default port for Redis.

const redis = require(‘redis’)
const client = redis.createClient(‘redis://localhost:6379’)

Let’s install the rest of the packages

npm install axios body-parser cors express redis

Finally, some code

const express = require('express')
const app = express()
const redis = require('redis')
const bodyParser = require('body-parser')
const cors = require('cors')
const axios = require('axios')
const client = redis.createClient('redis://localhost:6379')

app.use(cors())
app.use(bodyParser.urlencoded({ extended: true }))

// our client-site, with the form to submit a number to the server
app.get('/', (req, res) => {
    res.send(`
   <html lang="en">
      <head>
         <meta charset="UTF-8" />
         <meta name="viewport" content="width=device-width, initial-scale=1.0" />
         <meta http-equiv="X-UA-Compatible" content="ie=edge" />
         <title>Document</title>
      </head>
      <body>
         hi there
         <form action="/" method="post">
            <input type="number" name="number" placeholder="a number" />
            <input type="submit" />
         </form>
      </body>
   </html>
`)
})

// both functions get the original number from the user input, and the res object
// from express, to make a redirect to another URL as params
const getResultFromCache = (number, res) => {
    let CacheTime = Date.now()
    client.get(number, (error, result) => {
        if (result) {
            console.log(
                'Cache request took',
                Date.now() - CacheTime,
                'Milliseconds'
            )
            // redirect to display the result & source
            res.redirect('/done?result=' + result + '&from=cache')
        } else {
            console.log('error')
        }
    })
}

const getResultFromAPI = (number, res) => {
    let ApiTime = Date.now()
    axios
        .post('http://localhost:3000/', {
            number: number
        })
        .then(response => {
            console.log('API Request took', Date.now() - ApiTime, 'Milliseconds')
            let result = response.data.result
            // when receiving the result from API, original number from the user input and the result will be stored in the CACHE
            client.set(number, result)
            // the cache entry will be deleted after 60 sec, automatically
            client.expire(number, 60)
            res.redirect('/done?result=' + result + '&from=API')
        })
        .catch(error => {
            console.log(error)
        })
}

app.post('/', (req, res) => {
    let number = req.body.number
    // if the original number and the result are already stored, result will be true
    client.exists(number, (error, result) => {
        if (result) {
            getResultFromCache(number, res)
            // else we will make a request to the API
        } else {
            getResultFromAPI(number, res)
        }
    })
})

// the final page template, to which our functions redirect to
app.get('/done', (req, res) => {
    res.send(`
   <html>
      <head>
      </head>
      <body>
      The Result is: ${req.query.result}
      <br/>
      So the original value is ${req.query.result / 2}
      <br/>
      And comes from: ${req.query.from}
      </body>
   </html>
   `)
})

app.listen(8080)

server.js

Explaining the Most Important Parts

If we call up the start page of our servers in the browser (i.e. the client page), we do this via the /route (line 13 in the code). Above this, we get a form where we can enter a number that is sent to the server when submitting.

The number is sent as a POSTrequest to the server, which can process it from line 72 onwards thanks to body-parser.

With the client.exists function of Redis, we can check if a key — which we pass as the first parameter (number) — has a corresponding value stored. The result of the Redis query is returned as the result variable and is either true or false.

If the client has already stored a result for our number (result = true), we call the getResultFromCachefunction. If not, we call getResultFromAPI to make a query to the API.

getResultFromCache executes the client.get function of Redis to read the corresponding stored value from a key. Now we execute a redirect to our result page for the client, where we pass as URLparameter the result and the hint that it was read from the cache.

However, if our check of whether the number and its result is already cached says false, we run getResultFromAPI and execute an API request. Then we give the client the result of the API request and use client.set to store the key and the result of the multiplication by the API in the Redis cache.
With client.expire, we also set that the entry is automatically deleted after 60 seconds.

On line 86 of our result page if the client is generated, here we’re just giving the URL parameters that we pass from the two main functions of our API cache. For verification purposes, we calculate back to the original value via the result.

Not so spectacular: our API server

const express = require('express')
const bodyParser = require('body-parser')
const cors = require('cors')
const app = express()

app.use(cors())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())

app.post('/', (req, res) => {
    console.log(req.body.number)

    res.send({
        result: req.body.number * 2
    })
})

app.listen(3000)

Getting the Whole Thing to Work and Benchmarking It

Basically, our whole project consists only of the server.js and api.js. You should have both. They share the node_modules without problems, so after you have installed the dependencies as explained above, you can run both servers at the same time. At http://localhost:8080/, you can find the start page.

As you may have noticed, I saved the time in the two main functions everywhere with Date.now. Also, the process of the API fetching in the getResultFromAPI and reading from the cache in the
getResultFromCache starts.

At the end of the process, you simply subtract the stored start time with the current time after successful execution to get the duration of the whole process.

Comments

Popular posts from this blog

4 Ways to Communicate Across Browser Tabs in Realtime

1. Local Storage Events You might have already used LocalStorage, which is accessible across Tabs within the same application origin. But do you know that it also supports events? You can use this feature to communicate across Browser Tabs, where other Tabs will receive the event once the storage is updated. For example, let’s say in one Tab, we execute the following JavaScript code. window.localStorage.setItem("loggedIn", "true"); The other Tabs which listen to the event will receive it, as shown below. window.addEventListener('storage', (event) => { if (event.storageArea != localStorage) return; if (event.key === 'loggedIn') { // Do something with event.newValue } }); 2. Broadcast Channel API The Broadcast Channel API allows communication between Tabs, Windows, Frames, Iframes, and  Web Workers . One Tab can create and post to a channel as follows. const channel = new BroadcastChannel('app-data'); channel.postMessage(data); And oth...

Certbot SSL configuration in ubuntu

  Introduction Let’s Encrypt is a Certificate Authority (CA) that provides an easy way to obtain and install free  TLS/SSL certificates , thereby enabling encrypted HTTPS on web servers. It simplifies the process by providing a software client, Certbot, that attempts to automate most (if not all) of the required steps. Currently, the entire process of obtaining and installing a certificate is fully automated on both Apache and Nginx. In this tutorial, you will use Certbot to obtain a free SSL certificate for Apache on Ubuntu 18.04 and set up your certificate to renew automatically. This tutorial will use a separate Apache virtual host file instead of the default configuration file.  We recommend  creating new Apache virtual host files for each domain because it helps to avoid common mistakes and maintains the default files as a fallback configuration. Prerequisites To follow this tutorial, you will need: One Ubuntu 18.04 server set up by following this  initial ...

Working with Node.js streams

  Introduction Streams are one of the major features that most Node.js applications rely on, especially when handling HTTP requests, reading/writing files, and making socket communications. Streams are very predictable since we can always expect data, error, and end events when using streams. This article will teach Node developers how to use streams to efficiently handle large amounts of data. This is a typical real-world challenge faced by Node developers when they have to deal with a large data source, and it may not be feasible to process this data all at once. This article will cover the following topics: Types of streams When to adopt Node.js streams Batching Composing streams in Node.js Transforming data with transform streams Piping streams Error handling Node.js streams Types of streams The following are four main types of streams in Node.js: Readable streams: The readable stream is responsible for reading data from a source file Writable streams: The writable stream is re...