3.6 KiB
3.6 KiB
idempotency-middleware
(no submodules)
Purpose
Provides a framework-agnostic idempotency middleware factory that wraps request/response handlers with deduplication logic. createIdempotencyMiddleware reads an idempotency key from the request, checks the store, and returns the cached response or executes the handler and caches the result. Works with Express, Fastify, Hono, Fetch API, or any custom request/response abstraction.
Features
- Generic
TReq/TRestype parameters for full type safety - Configurable key extraction from any request type
- Per-operation TTL overrides for granular cache expiration
- Fingerprint mismatch detection before returning cached responses
transformResponsehook for adapter-specific response transformations- Bypass behaviour: if no key is extracted, the handler runs without caching
- State machine write (
pending→completed | failed) before executing handler
Usage: Basic Example
import { createIdempotencyMiddleware } from "@suaveplan/idempotency/middleware";
import { MemoryIdempotencyStore } from "@suaveplan/idempotency/store";
interface MyRequest { headers: Record<string, string>; body: unknown }
interface MyResponse { status: number; data: unknown }
const middleware = createIdempotencyMiddleware<MyRequest, MyResponse>({
store: new MemoryIdempotencyStore(),
extractKey: (req) => req.headers["idempotency-key"],
});
const result = await middleware(req, async (r) => ({ status: 200, data: await process(r.body) }));
console.log(result.cached); // true on second call with same key
Usage: Advanced Example
const middleware = createIdempotencyMiddleware<MyRequest, MyResponse>({
store,
extractKey: (req) => req.headers["idempotency-key"],
extractInput: (req) => req.body,
extractOperation: (req) => req.headers["x-operation"],
ttlOverrides: {
"create-payment": 86_400_000, // 24h
"send-email": 3_600_000, // 1h
},
transformResponse: (res) => ({ ...res, fromCache: true }),
});
API Reference
createIdempotencyMiddleware<TReq, TRes>(config): Handler
Returns async (request: TReq, handler: (req: TReq) => Promise<TRes>) => Promise<MiddlewareResult<TRes>>.
Config options:
| Option | Type | Default | Description |
|---|---|---|---|
store |
IdempotencyStoreAdapter |
required | Record persistence |
extractKey |
(req) => string | undefined |
required | Key extraction |
extractInput |
(req) => unknown |
— | Input for fingerprinting |
ttlMs |
number |
86_400_000 |
Default TTL |
ttlOverrides |
Record<string, number> |
— | Per-operation TTL |
extractOperation |
(req) => string | undefined |
— | Operation name for TTL lookup |
fingerprintFn |
fn |
fingerprintPayload |
Custom hasher |
transformResponse |
(res: TRes) => TRes |
— | Response transformer |
MiddlewareResult<TRes>
interface MiddlewareResult<TRes> {
response: TRes;
cached: boolean;
key: string;
}
Throws: IdempotencyMismatchError when the request fingerprint differs from the cached fingerprint.
Implementation Notes
- The middleware always writes a
pendingrecord before calling the handler, ensuring crash recovery on retry. failedrecords do not block retries — if the handler throws, subsequent requests with the same key re-execute the handler.- Bypassed requests (no key extracted) run the handler directly without touching the store.
transformResponseis applied both on cache hit and on fresh execution, so the response shape is always consistent.