Caching API Responses: Boosting Performance with HTTP Caching and 304 Status Codes
Web applications often rely on API calls to fetch and display dynamic data, such as product reviews, customer feedback, or pricing information. However, repeatedly fetching the same data for unchanged content can lead to unnecessary network latency and increased server load. To optimize performance, we can leverage HTTP caching mechanisms, similar to how browsers cache images and JavaScript/CSS files.
By using the Cache-Control header and ETags, we can instruct browsers to cache API responses effectively. When a client makes a subsequent request for the same resource, the server can respond with a 304 Not Modified status code if the content hasn't changed. This tells the browser to use its cached copy, reducing data transfer and improving loading times.
In this article, we'll explore how to implement API response caching using Cache-Control headers, ETags, and conditional requests.
Let's begin by examining a scenario without caching.
This Node.js server serves static files, index.html, from the "public" folder and provides a /getBook API endpoint to retrieve book information. Note: I have disabled etag automatic generation for the demonstration in this article.
//server.js
const fs = require('fs');
const books = JSON.parse(fs.readFileSync("./data.json"));
const express = require('express');
const app = express();
app.set('etag', false);
app.use(express.static('public')); //we serve index.html under public folder
app.get('/', (req, res, next) => {
res.status(200).send();
});
app.get('/getBook', (req, res) => {
console.log('Finding book: ' + req.query.id)
const data = books[req.query.id]
res.status(200).send(data);
});
app.listen(3000, () => {
console.log('Backend server listening on port 3000');
});
<!-- ./public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Fetch Example</title>
</head>
<body>
<input type="text" id="idInput" placeholder="Enter ID">
<button id="getDataButton">Get Data</button>
<div>
<h2 id="title"></h2>
<span id="longDescription"></span>
</div>
<script>
const idInput = document.getElementById('idInput');
const getDataButton = document.getElementById('getDataButton');
const title = document.getElementById('title');
const longDescription = document.getElementById('longDescription');
getDataButton.addEventListener('click', () => {
const id = idInput.value;
fetch(`/getBook?id=${id}`)
.then(response => response.json())
.then(data => {
title.textContent = `Title: ${data.title}`;
longDescription.textContent = data.longDescription;
})
.catch(error => {
resultDiv.textContent = 'Error fetching data';
});
});
</script>
</body>
</html>
To illustrate the issue, I clicked the "Get Data" button three times with the same ID. As you can see in the network tab, each click resulted in a separate API request to the server, even though the response data remained identical. This repetitive fetching leads to redundant network activity and increased latency, as evidenced by the consistent 200 OK responses. However, we can significantly optimize this behavior by implementing caching techniques.
To optimize our API calls, let's enable caching and use ETags. This technique allows the server to return a 304 Not Modified status code (without sending the response body) if the content hasn't changed since the client's last request. This approach significantly reduces data transfer and improves loading times.
The code bellow demonstrates how to implement ETag-based caching in an Express.js server. It generates an ETag for the response data and compares it with the If-None-Match header sent by the client. If the ETags match, the server sends a 304 response, signaling to the client that it can use its cached copy. Otherwise, it sends the full response with the new ETag.
app.get('/getBook', (req, res) => {
console.log('Finding book: ' + req.query.id)
const data = books[req.query.id]
const etag = generateEtag(data);
if (req.headers['if-none-match'] === etag) {
res.status(304).send();
} else {
//Set cache-control header
res.setHeader('Cache-Control', 'public, max-age=3600');
//Set etag header
res.setHeader('ETag', etag);
res.status(200).send(data);
}
});
When we first request data, the server responds with a 200 OK status and a 4.5 KB response body. This response includes Cache-Control and ETag headers, instructing the browser to cache the data. Subsequent requests then efficiently load the data from the browser's disk cache.
Recommended by LinkedIn
While this caching mechanism effectively reduces data transfer, it doesn't fully address my requirements. Ideally, I want the browser to always make API calls to the server, even if the data is cached, for two primary reasons:
To achieve this, I'll modify the caching strategy to allow the browser to make requests while still leveraging cached data when possible. The server will then determine whether to return the full data with a 200 OK status or signal that the data is unchanged with a 304 Not Modified status. We can modify our caching strategy on either the server-side or the client-side:
//server.js
...
app.get('/getBook', (req, res) => {
...
if (req.headers['if-none-match'] === etag) {
res.status(304).send();
} else {
// Set no-cache here
res.setHeader('Cache-Control', 'public, max-age=3600, no-cache');
...
}
});
// client-side code
<script>
const idInput = document.getElementById('idInput');
...
getDataButton.addEventListener('click', () => {
const id = idInput.value;
//Set cache option
fetch(`/getBook?id=${id}`, { cache: 'no-cache' })
....
});
</script>
After modifying the code, observe the network tab. The first request still results in a 200 OK status with the full response data. However, subsequent requests now trigger a server check, but instead of returning the full data again, the server responds with a 304 Not Modified status, indicating that the content hasn't changed. This efficiently avoids unnecessary data transfer while still allowing us to track API calls on the server.
Summary:
By implementing HTTP caching with ETags and Cache-Control headers, we gain several significant benefits:
Overall, HTTP caching is a valuable technique for optimizing web applications, leading to faster loading times, reduced server load, and a smoother user experience.