idempot-js is a monorepo containing framework-agnostic idempotency middleware with pluggable storage backends. This project uses pnpm workspaces for package management. The architecture follows a layered plugin pattern where:
- Core defines the idempotency logic and store interface
- Framework adapters integrate the middleware into web frameworks
- Storage backends persist idempotency records
This project uses pnpm workspaces (not npm workspaces). Key implications:
- Use
pnpm installinstead ofnpm install - Workspace packages are linked via
node_modules/@idempot/* - Dependencies between packages use
workspace:*protocol - Run
pnpm -r buildto build all packages - Each package has its own
package.jsonwith workspace dependencies
When creating a git worktree, always run pnpm install in the worktree directory to properly link workspace packages.
┌─────────────────────────────────────────────────────────────┐
│ Storage Backend Layer │
│ ┌──────────┐ ┌──────┐ ┌─────────┐ ┌──────┐ │
│ │ Redis │ │SQLite│ │ Postgres│ │ MySQL │ │
│ └──────────┘ └──────┘ └─────────┘ └──────┘ │
└─────────────────────────────────────────────────────────────┘
idempot-js/
├── packages/
│ ├── core/ # Framework-agnostic idempotency logic
│ │ ├── src/
│ │ │ ├── fingerprint.js # Request fingerprinting
│ │ │ ├── validation.js # Key validation & conflict detection
│ │ │ ├── resilience.js # Circuit breaker & retry logic
│ │ │ ├── interface.js # Store interface definition
│ │ │ └── default-options.js
│ │ └── tests/
│ │
│ ├── frameworks/ # Framework adapters
│ │ ├── hono/
│ │ ├── express/
│ │ ├── bun/
│ │ └── fastify/
│ │
│ └── stores/ # Storage backend implementations
│ ├── sqlite/
│ ├── redis/
│ ├── postgres/
│ ├── mysql/
│ └── bun-sql/
│
├── examples/ # Usage examples
└── tests/ # Integration tests
The core package contains the framework-agnostic idempotency logic. All framework adapters use these core functions.
Important: @idempot/core is an internal shared package, not a user-facing package. Users should never install or import from it directly.
- Users install: Framework packages (
@idempot/hono-middleware,@idempot/express-middleware) and store packages (@idempot/sqlite-store,@idempot/redis-store) - Core is: A transitive dependency that framework and store packages use internally
User Application
│
├─► @idempot/hono-middleware ──► @idempot/core (transitive)
│ │
└─► @idempot/sqlite-store ──────► @idempot/core (transitive)
Generates a deterministic fingerprint from the request body. This fingerprint detects when the same idempotency key is used with different payloads.
Key features:
- Uses SHA-256 hashing for collision resistance
- Supports JSONPath expressions for field exclusion
- Handles non-JSON payloads (plain text, etc.)
- Canonicalizes JSON to ensure consistent hashing
Handles request validation and conflict detection. Functions include idempotency key validation, field exclusion validation, conflict detection, and cached response handling.
Provides resilience against storage backend failures using opossum circuit breaker:
Features:
- Retry logic: Failed operations retry up to 3 times with configurable delay
- Timeout: Operations timeout after 500ms (configurable) to prevent hanging
- Circuit breaker: Opens after 50% failure rate over 10 requests
- Fail-fast: While circuit is open, requests fail immediately without calling store
- Auto-recovery: Circuit resets after 30 seconds
Defines the IdempotencyStore interface that all storage backends must implement. The interface includes methods for lookup, startProcessing, and complete.
Each framework adapter implements the idempotency middleware pattern for its framework.
Key characteristics:
- Uses Hono's middleware pattern
- Accesses request via
c.reqcontext - Returns responses via
c.json()orc.body() - Exposes circuit breaker via
middleware.circuit
Key characteristics:
- Uses Express middleware pattern (
(req, res, next)) - Accesses request via
reqandresobjects - Handles async handlers properly
- Supports
preHandlerstyle for Fastify compatibility
Key characteristics:
- Uses Bun's
Request => Responsehandler pattern - Returns a wrapper function that wraps handlers with idempotency enforcement
- Re-creates the request after body consumption so handlers can read the body
- Exposes circuit breaker via
wrap.circuit
Key characteristics:
- Uses Fastify's
preHandlerhook - Handles both sync and async handlers
- Properly clones responses for caching
All storage backends implement the IdempotencyStore interface. Each has different characteristics:
Best for: Single-server, local development, lightweight applications
Implementation:
- Uses
better-sqlite3(synchronous, high-performance) - Stores records in a single table with indexes on
fingerprintandexpires_at - JSON-serializes response headers for storage
Persistence considerations: Use litestream or similar for production deployment.
Best for: High-performance, distributed systems, microservices
Implementation:
- Uses
ioredisclient - Stores two keys per request: one by key, one by fingerprint
- Uses Redis TTL for automatic expiration
Persistence considerations: Configure AOF (Append Only File) for reliability.
Best for: Multi-server deployments, existing Postgres infrastructure
Implementation:
- Uses
pgpool for connection management - JSONB column for response headers
- Indexed queries on key and fingerprint
Similar schema to SQLite but with:
- JSONB type for headers (more efficient than TEXT)
- Connection pooling via
pg.Pool
Best for: Multi-server deployments, existing MySQL infrastructure
Implementation:
- Node.js:
MysqlIdempotencyStoreusesmysql2with connection pooling - Deno:
MysqlIdempotencyStoreusesmysqlfrom deno.land/x (import via@idempot/mysql-store/deno-mysql.js) - JSON column for response headers
- Indexed queries on key and fingerprint
Similar schema to PostgreSQL but with:
- MySQL-specific syntax (backticks, different JSON handling)
- Connection pooling via
mysql2Pool or Deno'smysqlclient
Best for: Bun runtime applications with SQLite, PostgreSQL, or MySQL
Implementation:
- Uses Bun's native SQL API via
bun:sql - Supports SQLite, PostgreSQL, and MySQL via connection string
- Auto-detects adapter from URL format
- Maximum performance for Bun environment
- Request arrives at framework adapter
- Framework adapter calls middleware
- Core: Extract idempotency key from header
- Core: Validate key length (16-255 chars)
- Core: Generate fingerprint from request body
- Core: Call store.lookup(key, fingerprint)
- Store: Query database by key and fingerprint
- Core: Check for conflicts (none found)
- Core: Call store.startProcessing(key, fingerprint, ttlMs)
- Store: Insert record with status='processing'
- Core: Call next() to pass to handler
- Handler: Execute business logic
- Framework adapter: Capture response
- Core: Clone response body and headers
- Core: Call store.complete(key, response)
- Store: Update record with status='complete' and response data
- Framework adapter: Return response to client
1-7. Same as above 8. Core detects byKey exists with same fingerprint 9. Core extracts cached response 10. Core adds x-idempotent-replayed: true header 11. Framework adapter returns cached response without calling handler
1-7. Same as above 8. Core detects byKey exists with different fingerprint 9. Core returns 422 Unprocessable Entity with error message 10. Framework adapter returns error response
- Request A arrives, starts processing
- Request B arrives with same key
- Core detects byKey with status='processing'
- Core returns 409 Conflict (request already in progress)
- Client should retry after delay
The library follows draft-ietf-httpapi-idempotency-key-header-07:
- Uses
Idempotency-Keyheader (case-insensitive) - Returns
409 Conflictfor concurrent processing - Returns
422 Unprocessable Entityfor key reuse with different payload - Adds
x-idempotent-replayed: trueheader on cached responses
Default 21-255 character range (nanoid default) provides ~126 bits of entropy, preventing:
- Key exhaustion attacks (short keys)
- Collision attacks (insufficient entropy)
The minimum of 21 is enforced at middleware initialization - values below 21 will throw an error.
The circuit breaker pattern provides graceful degradation:
- Fail-fast on storage failures - Prevents cascading failures
- Retry with backoff - Transient failures are handled automatically
- Automatic recovery - System recovers when storage becomes available
- Monitoring - Exposes circuit state for observability
- Node.js: Full support via better-sqlite3, ioredis, pg, mysql2
- Bun: Native SQLite via
bun:sqlite, ioredis support - Deno: Native SQLite via
deno-sqlite, native Redis support, MySQL via@idempot/mysql-store/deno-mysql.js - AWS Lambda: Planned (DynamoDB or Redis via AWS SDK)
- Cloudflare Workers: Planned (KV storage)
The IdempotencyStore interface is intentionally simple:
- Key-based lookups enable O(1) performance
- Fingerprint-based lookups detect key collisions
- Atomic operations prevent race conditions
- TTL-based expiration avoids manual cleanup
Framework adapters follow middleware patterns where idempotency wraps the handler and can be combined with other middleware like authentication.
The middleware:
- Checks request method (only POST/PATCH protected)
- Validates idempotency key (if present)
- Generates fingerprint
- Checks storage for conflicts or cached response
- Either returns cached response or calls handler
All framework adapters (Hono, Express, Fastify) share a common test suite defined in packages/core/tests/framework-adapter-suite.js. This ensures consistent behavior across all adapters while reducing test code duplication by ~85%.
Usage:
import { runAdapterTests } from "@idempot/core/tests/framework-adapter-suite.js";
runAdapterTests({
name: "framework-name",
setup: async () => ({
mount: (method, path, middleware, handler) => {
/* framework-specific */
},
request: async (options) => {
/* return { status, headers, body } */
},
teardown: async () => {
/* cleanup */
}
}),
createMiddleware: (options) => idempotency(options),
createStore: () => new SqliteIdempotencyStore({ path: ":memory:" })
});Test Coverage (20 shared tests):
- HTTP method handling (GET pass-through)
- Key validation (length, empty, commas, required/optional)
- Caching behavior (first request, replay, call count)
- Conflict detection (concurrent, payload mismatch, fingerprint collision)
- Error handling (lookup failure, startProcessing failure, complete failure)
- Configuration (field exclusion, PATCH method, body types)
Benefits:
- Consistent behavior across all adapters
- ~85% reduction in test code (1,822 lines → ~280 lines)
- Easy to add new framework adapters (only ~30 lines needed)
- Single source of truth for idempotency behavior
- Core functions tested in isolation
- Storage backends tested with mocks
- Edge cases (empty body, non-JSON, timeouts)
- Each framework adapter tested with real handlers
- Each storage backend tested with real database
- End-to-end request/response cycles
- Bun-specific tests run with Bun runtime
- Deno-specific tests run with Deno
- AWS Lambda and Cloudflare Workers tests planned
For local development, integration tests run against Redis and Postgres containers managed by apple/container.
Container Configuration:
- Container name:
idempot-js-dev - Redis:
arm64v8/redis:7-alpineon port 6379 - Postgres:
postgres:16-alpineon port 5432 - Database:
testwith useridempot/ passwordidempot
npm Scripts:
| Script | Description |
|---|---|
test:container:start |
Start Redis and Postgres containers |
test:container:stop |
Stop and remove containers |
test:container:restart |
Restart containers |
test:integration |
Run integration tests |
Usage:
# Start containers, run tests, stop containers
npm run test:container:start && npm run test:integration && npm run test:container:stop
# Just start containers for manual testing
npm run test:container:startThe containers use arm64 architecture (native to Apple Silicon) to avoid Rosetta translation overhead.
- Copy the
IdempotencyStoreinterface pattern from an existing store (e.g.,packages/stores/sqlite/index.js) - Implement all methods:
lookup,startProcessing,complete,close - Follow existing patterns (see
packages/stores/sqlite/for a reference implementation) - Write unit tests using the shared test patterns
- Add to
examples/directory - Update README with backend matrix
Note: Do not import IdempotencyStore from @idempot/core. Define the interface locally in your store implementation, matching the pattern used by existing stores.
- Import core functions from
@idempot/core - Implement middleware following framework pattern
- Handle request/response lifecycle
- Expose circuit breaker for monitoring
- Write integration tests
Override excludeFields to customize what's included in fingerprint. Supports JSONPath expressions for root-level fields, nested fields, and deep nesting.