Files
skills/docs/foundation/flow/idempotency/src/middleware/idempotency-middleware.md
2026-05-29 08:20:56 +01:00

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 / TRes type 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
  • transformResponse hook 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 pending record before calling the handler, ensuring crash recovery on retry.
  • failed records 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.
  • transformResponse is applied both on cache hit and on fresh execution, so the response shape is always consistent.