Idempotency middleware for Hono, Express, and Fastify.
- Implements the IETF draft draft-ietf-httpapi-idempotency-key-header-07 specification
- Request fingerprinting for conflict detection
- Built-in resilience: retries, timeouts, circuit breaker
- Modular packages reduce install time and dependencies
This library uses JavaScript with JSDoc comments for type information. Each package ships .d.ts declaration files generated from the JSDoc-annotated source.
TypeScript picks them up automatically via the types field in each package.json.
import { idempotency } from "@idempot/hono-middleware";The declarations are generated at publish time to ensure types match the published version.
| Category | Options |
|---|---|
| Runtimes | Node.js, Bun, Deno (Lambda & Cloudflare Workers planned) |
| Frameworks | Express, Hono, Fastify, Bun Server |
| Stores | Redis, PostgreSQL, MySQL, SQLite (DynamoDB & Cloudflare KV planned) |
Duplicate requests return cached responses with x-idempotent-replayed: true.
Error responses follow RFC 9457 (Problem Details for HTTP APIs) and include:
type- URI identifying the problem typetitle- Short human-readable summarydetail- Detailed explanationstatus- HTTP status codeinstance- Unique identifier for this error occurrenceretryable- Whether retrying might succeedidempotency_key- The idempotency key from the request (when applicable)
The middleware supports content negotiation via the Accept header:
Default (application/problem+json):
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-d '{"item": "widget"}'Response:
{
"type": "https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-07#section-2.1",
"title": "Idempotency-Key is missing",
"detail": "This operation is idempotent and it requires correct usage of Idempotency Key.",
"status": 400,
"instance": "urn:uuid:550e8400-e29b-41d4-a716-446655440000",
"retryable": false
}Markdown format (for AI agents):
curl -X POST http://localhost:3000/orders \
-H "Accept: text/markdown" \
-H "Content-Type: application/json" \
-d '{"item": "widget"}'Response:
---
type: "https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-07#section-2.1"
status: 400
instance: "urn:uuid:550e8400-e29b-41d4-a716-446655440000"
retryable: false
---
# Idempotency-Key is missing
## What Happened
This operation is idempotent and it requires correct usage of Idempotency Key.
## What You Should Do
**Correct the issue.** This error requires changes to your request. Do not retry with the same idempotency key until the issue is resolved.The markdown format includes YAML frontmatter with all error fields and human-readable guidance for AI agents.
npm install @idempot/hono-middleware @idempot/sqlite-storeimport { Hono } from "hono";
import { idempotency } from "@idempot/hono-middleware";
import { SqliteIdempotencyStore } from "@idempot/sqlite-store";
const app = new Hono();
const store = new SqliteIdempotencyStore({ path: ":memory:" });
app.post("/orders", idempotency({ store }), async (c) => {
return c.json({ id: "order-123" }, 201);
});The middleware accepts an options object with the following properties:
| Option | Type | Default | Description |
|---|---|---|---|
store |
IdempotencyStore |
required | Storage backend (Redis, PostgreSQL, MySQL, SQLite) |
required |
boolean |
true |
Whether the Idempotency-Key header is required |
ttlMs |
number |
86400000 (24 hours) |
Time-to-live for idempotency records in milliseconds |
minKeyLength |
number |
21 |
Minimum length for idempotency keys |
maxKeyLength |
number |
255 |
Maximum length for idempotency keys |
excludeFields |
string[] |
[] |
Body fields to exclude from request fingerprint |
resilience |
object |
see below | Circuit breaker and retry configuration |
Resilience options:
| Option | Type | Default | Description |
|---|---|---|---|
timeoutMs |
number |
500 |
Timeout per store operation |
maxRetries |
number |
3 |
Retry attempts for failed operations |
retryDelayMs |
number |
100 |
Delay between retries |
errorThresholdPercentage |
number |
50 |
Error rate to trigger circuit breaker |
resetTimeoutMs |
number |
30000 |
Time before testing recovery |
volumeThreshold |
number |
10 |
Minimum requests before circuit can open |
Example with custom configuration:
app.post(
"/orders",
idempotency({
store,
ttlMs: 7 * 24 * 60 * 60 * 1000, // 7 days
excludeFields: ["timestamp", "$.metadata.requestId"],
resilience: {
timeoutMs: 1000,
maxRetries: 5
}
}),
handler
);Monitor circuit breaker state:
const middleware = idempotency({ store });
console.log(middleware.circuit.status); // 'closed', 'open', or 'half-open'See the full configuration guide for detailed documentation.
See the examples/ directory for complete examples.
See GitHub Releases for the changelog.
BSD-3