typescript 129 lines · 3 tabs

Idempotent POST Requests in Express with a Redis-Backed Middleware

Shared by codesnips Aug 2026
3 tabs
import type { Redis } from "ioredis";

export interface StoredResponse {
  status: "pending" | "completed";
  fingerprint: string;
  httpStatus?: number;
  headers?: Record<string, string>;
  body?: unknown;
}

export class IdempotencyStore {
  private readonly pendingTtlMs = 30_000;
  private readonly completedTtlMs = 24 * 60 * 60 * 1000;

  constructor(private readonly redis: Redis) {}

  private redisKey(key: string): string {
    return `idem:${key}`;
  }

  async begin(key: string, fingerprint: string): Promise<{ claimed: boolean; existing?: StoredResponse }> {
    const record: StoredResponse = { status: "pending", fingerprint };
    const ok = await this.redis.set(this.redisKey(key), JSON.stringify(record), "PX", this.pendingTtlMs, "NX");
    if (ok === "OK") return { claimed: true };

    const raw = await this.redis.get(this.redisKey(key));
    return { claimed: false, existing: raw ? (JSON.parse(raw) as StoredResponse) : undefined };
  }

  async complete(key: string, response: Omit<StoredResponse, "status">): Promise<void> {
    const record: StoredResponse = { status: "completed", ...response };
    await this.redis.set(this.redisKey(key), JSON.stringify(record), "PX", this.completedTtlMs);
  }

  async release(key: string): Promise<void> {
    await this.redis.del(this.redisKey(key));
  }
}
3 files · typescript Explain with highlit

Idempotency keys let a client safely retry a POST without risking a second charge or duplicate record. The idea: the client generates a stable key (usually a UUID) and sends it in an Idempotency-Key header. The server records the first response under that key and replays it for any later request carrying the same key, so retries after a timeout or dropped connection become no-ops that return the original result.

The IdempotencyStore tab wraps Redis and encodes the state machine each key moves through. begin uses SET key value NX PX to atomically claim a key in the pending state; if the SET fails, another request already owns it and the caller learns the current record instead. This atomic claim is the crux — it collapses the check-and-set race that would otherwise let two concurrent retries both proceed. complete overwrites the record with the captured status, headers, and body and gives it a longer TTL so genuine retries within the retention window replay successfully, while release clears a pending slot when the handler throws so the client can try again cleanly.

The idempotency middleware tab drives that store. It only engages for unsafe methods, and returns 400 when the header is missing on a route that requires it. It also fingerprints the request body with sha256 and stores it alongside the response; if the same key arrives with a different payload, it responds 422 rather than silently returning a mismatched result — a subtle but important guard against key reuse bugs. When a record is already completed, it short-circuits and replays the stored response. To capture the response transparently it monkey-patches res.json, buffering the payload so it can persist the outcome exactly once the handler succeeds.

The payments route tab shows the realistic payoff: a charge endpoint wrapped by idempotency({ required: true }), where the actual side effect runs only on the first request. The trade-offs worth noting are TTL choice (too short and legitimate retries miss the cache), the pending window returning 409 under concurrent duplicates, and the fact that non-deterministic handlers should persist their own result rather than trust replay. This pattern is standard in payment and webhook APIs where exactly-once semantics over an at-least-once network matter.


Related snips

Share this code

Here's the card — post it anywhere.

Idempotent POST Requests in Express with a Redis-Backed Middleware — share card
Link copied