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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Fingerprinting the body lets you distinguish a genuine retry from a key reused with different data.
- 2Recording a 'processing' state before work begins guards against concurrent duplicates and lets you replay completed results.
- 3Wrapping res.json is a clean way to capture a response for caching without touching every handler.
Related explainers
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
ruby
class LogAggregator BUCKET_FORMAT = "%Y-%m-%dT%H:%M" def initialize(entries)
Bucketing log entries by the minute in Ruby
aggregation
hashing
enumerable
Intermediate
5 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
ruby
class ApplicationController < ActionController::Base EXPERIMENTS = { checkout_button_color: %w[control blue green], onboarding_flow: %w[control streamlined]
How A/B test cohorts are assigned in Rails
a-b-testing
cookies
hashing
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/idempotent-post-requests-in-express-explained-javascript-fdbc/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.