idempotency-guard
v1.0.0
Published
Drop-in Express middleware that prevents duplicate processing of retried API requests using idempotency keys.
Downloads
13
Maintainers
Readme
idempotency-guard
Drop-in Express middleware that prevents duplicate processing of retried API requests.
Networks are unreliable. Clients time out and retry. Mobile apps double-tap "Pay Now." Load balancers occasionally deliver the same request twice. Without protection, any of these can double-charge a card, double-book a seat, or duplicate a record. This middleware closes that gap using the industry-standard Idempotency-Key pattern (the same approach used by Stripe and other payment APIs).
Install
npm install idempotency-guardQuick start
const express = require('express');
const { createIdempotencyMiddleware, MemoryStore } = require('idempotency-guard');
const app = express();
app.use(express.json());
const store = new MemoryStore(); // swap for MongoStore in production
app.use(createIdempotencyMiddleware({ store }));
app.post('/charge', (req, res) => {
const charge = chargeCard(req.body);
res.status(201).json(charge);
});Clients call it like this:
POST /charge
Idempotency-Key: 3f29-a91c-...
{ "amount": 500 }If the same key + same body is sent again (retry), the middleware returns the
original response without re-running chargeCard. If the same key is sent with a
different body, it's rejected with 409 — that's a bug on the client, not a retry.
Production store (MongoDB)
const { MongoClient } = require('mongodb');
const { MongoStore } = require('idempotency-guard');
const client = await MongoClient.connect(process.env.MONGO_URL);
const collection = client.db('app').collection('idempotency_keys');
await MongoStore.createIndexes(collection); // unique key index + TTL index
const store = new MongoStore(collection);MemoryStore is fine for local dev or a single-instance service. Anything running more
than one instance needs MongoStore (or a similar shared store) so two instances can't
both claim the same key.
How it decides what to do
| Situation | Response |
|---|---|
| New key | Runs your handler normally, caches the result |
| Same key, same payload, already finished | Replays the cached response (Idempotent-Replay: true header), your handler does not run again |
| Same key, still processing (concurrent retry) | 409 request_in_progress |
| Same key, different payload | 409 idempotency_key_reused |
| No key on the request | Passes through untouched — idempotency is opt-in per client |
Options
createIdempotencyMiddleware({
store, // required: MemoryStore or MongoStore
headerName: 'Idempotency-Key', // header the client sends
ttlMs: 24 * 60 * 60 * 1000, // how long a key is remembered (24h default)
methods: ['POST', 'PATCH', 'PUT'] // which methods get checked
})Run the demo
npm install
npm run start:exampleThen in another terminal:
curl -X POST http://localhost:3000/charge \
-H "Content-Type: application/json" \
-H "Idempotency-Key: demo-key-1" \
-d '{"amount": 500}'
# run it again with the same key — same chargeId comes back
curl -X POST http://localhost:3000/charge \
-H "Content-Type: application/json" \
-H "Idempotency-Key: demo-key-1" \
-d '{"amount": 500}'Tests
npm testLicense
MIT
