javascript 54 lines · 8 steps

Idempotent POST requests in Express

A middleware factory that safely replays duplicate POST requests using an Idempotency-Key and a cache store.

Explained by highlit
1const crypto = require('crypto');
2 
3function idempotency({ store, ttlMs = 24 * 60 * 60 * 1000 }) {
4 return async function idempotencyMiddleware(req, res, next) {
5 if (req.method !== 'POST') return next();
6 
7 const key = req.get('Idempotency-Key');
8 if (!key) {
9 return res.status(400).json({ error: 'Idempotency-Key header is required' });
10 }
11 
12 const fingerprint = crypto
13 .createHash('sha256')
14 .update(JSON.stringify(req.body ?? {}))
15 .digest('hex');
16 const cacheId = `idem:${req.path}:${key}`;
17 
18 const existing = await store.get(cacheId);
19 if (existing) {
20 if (existing.fingerprint !== fingerprint) {
21 return res.status(422).json({
22 error: 'Idempotency-Key already used with a different request body',
23 });
24 }
25 if (existing.status === 'completed') {
26 res.set('Idempotent-Replayed', 'true');
27 return res.status(existing.statusCode).json(existing.body);
28 }
29 return res.status(409).json({ error: 'A request with this key is still processing' });
30 }
31 
32 await store.set(cacheId, { fingerprint, status: 'processing' }, ttlMs);
33 
34 const originalJson = res.json.bind(res);
35 res.json = (body) => {
36 store
37 .set(
38 cacheId,
39 { fingerprint, status: 'completed', statusCode: res.statusCode, body },
40 ttlMs,
41 )
42 .catch(() => store.delete(cacheId));
43 return originalJson(body);
44 };
45 
46 res.on('close', () => {
47 if (!res.writableEnded) store.delete(cacheId).catch(() => {});
48 });
49 
50 next();
51 };
52}
53 
54module.exports = idempotency;
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Fingerprinting the body lets you distinguish a genuine retry from a key reused with different data.
  2. 2Recording a 'processing' state before work begins guards against concurrent duplicates and lets you replay completed results.
  3. 3Wrapping res.json is a clean way to capture a response for caching without touching every handler.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Idempotent POST requests in Express — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code