patterntypescriptexpressModerate
ETag and Conditional Requests for Efficient Caching
Viewed 0 times
ETagIf-None-MatchIf-Match304 Not Modifiedconditional requestcache
Problem
API responses are transferred in full on every request even when the data hasn't changed, wasting bandwidth and server processing.
Solution
Implement ETags and If-None-Match headers for conditional GET requests.
import crypto from 'node:crypto';
function generateETag(data: unknown): string {
return crypto
.createHash('md5')
.update(JSON.stringify(data))
.digest('hex');
}
app.get('/users/:id', async (req, res) => {
const user = await db.getUser(req.params.id);
const etag = `"${generateETag(user)}"`;
// Check if client has current version
if (req.headers['if-none-match'] === etag) {
return res.status(304).end(); // Not Modified — no body sent
}
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
res.json(user);
});
// Conditional PUT to prevent lost updates
app.put('/users/:id', async (req, res) => {
const ifMatch = req.headers['if-match'];
const current = await db.getUser(req.params.id);
const currentETag = `"${generateETag(current)}"`;
if (ifMatch && ifMatch !== currentETag) {
return res.status(412).json({ error: 'Precondition Failed' });
}
// Proceed with update...
});Why
ETags are opaque identifiers for a specific version of a resource. If-None-Match sends the ETag back; if unchanged the server returns 304 with no body, saving transfer. If-Match prevents overwriting concurrent changes.
Gotchas
- Weak ETags (W/"...") allow semantic equivalence; strong ETags require byte-for-byte identity.
- ETag generation must be fast — hashing large payloads on every request is expensive. Cache or derive from DB version columns.
- ETags must be quoted strings in HTTP headers.
Revisions (0)
No revisions yet.