javascript 47 lines · 7 steps

Optimistic locking with ETags in Express

A PUT route uses If-Match and ETags to reject updates when a document changed underneath the client.

Explained by highlit
1const express = require('express');
2const crypto = require('crypto');
3const router = express.Router();
4 
5const db = require('../db');
6 
7function etagFor(document) {
8 return crypto
9 .createHash('sha1')
10 .update(`${document.id}:${document.version}`)
11 .digest('base64');
12}
13 
14router.put('/documents/:id', express.json(), async (req, res) => {
15 const ifMatch = req.get('If-Match');
16 if (!ifMatch) {
17 return res
18 .status(428)
19 .json({ error: 'If-Match header is required for updates' });
20 }
21 
22 const document = await db.documents.findById(req.params.id);
23 if (!document) {
24 return res.sendStatus(404);
25 }
26 
27 const currentTag = etagFor(document);
28 const provided = ifMatch.split(',').map((t) => t.trim().replace(/^W\//, ''));
29 if (!provided.includes(`"${currentTag}"`) && !provided.includes('*')) {
30 res.set('ETag', `"${currentTag}"`);
31 return res.status(412).json({
32 error: 'Document was modified by another request',
33 currentVersion: document.version,
34 });
35 }
36 
37 const updated = await db.documents.update(document.id, {
38 title: req.body.title,
39 body: req.body.body,
40 version: document.version + 1,
41 });
42 
43 res.set('ETag', `"${etagFor(updated)}"`);
44 res.json(updated);
45});
46 
47module.exports = router;
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1ETags derived from a version let the server detect concurrent modifications without locking rows.
  2. 2Requiring If-Match on writes turns lost-update races into explicit 412 conflicts the client can retry.
  3. 3Bumping the version on every write keeps each ETag unique and makes stale writes detectable.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Optimistic locking with ETags in Express — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code