Idempotency Middleware for Express
raw JSON →express-idempotency is an Express.js middleware designed to add idempotency to API routes, inspired by Stripe's implementation. It ensures that repeated requests with the same idempotency key result in the same outcome, preventing duplicate processing of non-idempotent operations. The current stable version is 2.0.0. The package has seen consistent updates, with recent releases focusing on dependency upgrades and security fixes, indicating an active maintenance cadence rather than rapid feature development. Key differentiators include its high level of customization for data adapters, intent validators, and response validators, allowing developers to integrate it deeply with their existing infrastructure and persistence layers. It also provides helpers to detect idempotency hits and report errors, facilitating robust error handling within route handlers. It requires Node.js >=18.0.0 and npm >=9.0.0, aligning with modern JavaScript ecosystem standards. The library ships with TypeScript types, promoting strong typing in projects.
Common errors
error TypeError: Cannot read properties of undefined (reading 'idempotency') ↓
const { idempotency } = require('express-idempotency'); or const idempotency = require('express-idempotency').idempotency;. For ESM, use import { idempotency } from 'express-idempotency';. error Idempotency key misuse detected (HTTP 409 Conflict) ↓
Idempotency-Key and *identical* request parameters (method, URL, query, body). If the intent or payload of the request changes, a new, unique Idempotency-Key must be generated and used. error ReferenceError: getSharedIdempotencyService is not defined ↓
app.use(idempotency(...)) has been called to initialize the middleware before attempting to call getSharedIdempotencyService(). If using TypeScript, you might need a declare function getSharedIdempotencyService(): ...; statement in a type definition file or at the top of your file to resolve type checking issues if it's implicitly global. Warnings
breaking Version 2.0.0 updated the minimum required Node.js version to `>=18.0.0` and npm to `>=9.0.0`. Projects running on older environments must upgrade their Node.js and npm installations. ↓
gotcha The default data adapter provided by `express-idempotency` is an in-memory store, which is explicitly stated as not suitable for production environments due to lack of persistence across restarts and inability to scale. Using it in production can lead to lost idempotency state. ↓
gotcha The idempotency middleware is designed to always call `next()` in the Express chain, even if an idempotency hit is detected and a cached response is sent. Developers must explicitly check `idempotencyService.isHit(req)` within their route handler and `return` early to prevent accidental execution of business logic for replayed requests. ↓
gotcha The `getSharedIdempotencyService()` helper function's availability and exact mechanism can be a source of confusion. It implies a global singleton, which might conflict with certain testing patterns or specific dependency injection setups. ↓
security The security and reliability of idempotency heavily depend on clients generating truly unique and unpredictable `Idempotency-Key` headers. Reusing the same key for different operations or using weak/predictable keys can lead to security vulnerabilities or bypass the idempotency mechanism. ↓
Install
npm install express-idempotency yarn add express-idempotency pnpm add express-idempotency Imports
- idempotency wrong
const idempotency = require('express-idempotency'); // This tries to call the module directly, not the named exportcorrectimport { idempotency } from 'express-idempotency'; - IIdempotencyDataAdapter wrong
import { IIdempotencyDataAdapter } from 'express-idempotency'; // Imports as a value instead of a typecorrectimport type { IIdempotencyDataAdapter } from 'express-idempotency'; - getSharedIdempotencyService wrong
import { getSharedIdempotencyService } from 'express-idempotency'; // This function is not directly exported as a module membercorrectconst idempotencyService = getSharedIdempotencyService();
Quickstart
import express from 'express';
import { idempotency, IIdempotencyDataAdapter } from 'express-idempotency';
import { v4 as uuidv4 } from 'uuid';
// IMPORTANT: For production, replace this with a persistent storage solution (Redis, MongoDB, etc.)
// The default in-memory adapter is not suitable for production environments.
class InMemoryDataAdapter implements IIdempotencyDataAdapter {
private store = new Map<string, { request: any; response: any; status: string }>();
async get(idempotencyKey: string): Promise<{ request: any; response: any; status: string } | null> {
return this.store.get(idempotencyKey) || null;
}
async set(idempotencyKey: string, request: any, response: any, status: string): Promise<void> {
this.store.set(idempotencyKey, { request, response, status });
}
async remove(idempotencyKey: string): Promise<void> {
this.store.delete(idempotencyKey);
}
}
const app = express();
const port = 3000;
// Middleware to parse JSON bodies
app.use(express.json());
// Initialize the idempotency middleware. Always provide a production-ready data adapter.
app.use(
idempotency({
dataAdapter: new InMemoryDataAdapter(), // Replace with a real data adapter for production!
// Other options like idempotencyKeyHeader, intentValidator, responseValidator can be customized here.
})
);
// Declare `getSharedIdempotencyService` for TypeScript if it's globally available.
// In a real application, consider explicitly importing a service factory if available,
// or accessing a request-scoped service if the middleware attaches it (e.g., `req.idempotencyService`).
declare function getSharedIdempotencyService(): {
isHit(req: express.Request): boolean;
reportError(req: express.Request): void;
};
app.post('/process-payment', (req, res) => {
// Retrieve the IdempotencyService instance.
const idempotencyService = getSharedIdempotencyService();
// Crucial: Check if the request is an idempotency hit. If so, prevent further processing.
if (idempotencyService.isHit(req)) {
console.log('Idempotency hit detected, a cached response should have been sent by the middleware.');
// The middleware is designed to send the cached response and then call next().
// Your route handler should return early here to avoid re-executing business logic.
return;
}
// --- Your business logic starts here (only for non-idempotent requests) ---
console.log('Processing new payment for:', req.body);
const transactionId = uuidv4();
const amount = req.body.amount;
if (amount <= 0) {
// If an error occurs during processing, report it to the middleware
// so that the idempotency state for this key can be cleared or updated.
idempotencyService.reportError(req);
return res.status(400).json({ error: 'Amount must be positive.' });
}
// Simulate an asynchronous payment processing operation
setTimeout(() => {
const responseData = {
message: `Payment for ${amount} processed successfully.`,
transactionId: transactionId,
status: 'completed',
};
res.status(200).json(responseData);
console.log('Payment processed and response sent.');
}, 1000);
});
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
console.log(`
To test, send a POST request with an 'Idempotency-Key' header:`)
console.log(`curl -X POST -H "Content-Type: application/json" -H "Idempotency-Key: my-unique-key-123" -d '{"amount": 100}' http://localhost:${port}/process-payment`);
console.log(`
Repeat the curl command with the *same* 'my-unique-key-123' to observe idempotency in action (cached response).`);
console.log(`Use a *different* key for a new payment.`);
});