[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build an Idempotent API with Request Deduplication

Build an Idempotent API with Request Deduplication

Build idempotent API endpoints that safely handle retries and duplicate requests — preventing double charges, duplicate records, and race conditions in payment and order processing.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Maya leads payments at a 30-person marketplace. Users click "Pay" and the request times out, so they click again. Result: double charge. The mobile app retries failed requests automatically — creating duplicate orders. A network hiccup between the API and Stripe means the payment succeeds but the API returns a 500, so the client retries and charges the customer twice. Last month: 47 double charges, $12K in refunds, and angry customer emails. They need every mutating API endpoint to be idempotent — the same request processed twice produces the same result.

Step 1: Build the Idempotency Middleware

typescript
// src/middleware/idempotency.ts — Idempotency key middleware for safe retries
import { Context, Next } from "hono";
import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

interface IdempotencyRecord {
  status: "processing" | "completed";
  statusCode: number;
  body: string;
  headers: Record<string, string>;
  createdAt: number;
}

const IDEMPOTENCY_TTL = 86400; // 24 hours

export function idempotency() {
  return async (c: Context, next: Next) => {
    // Only apply to mutating methods
    if (!["POST", "PUT", "PATCH", "DELETE"].includes(c.req.method)) {
      return next();
    }

    const idempotencyKey = c.req.header("Idempotency-Key");

    // No key = no idempotency protection (backward compatible)
    if (!idempotencyKey) return next();

    // Validate key format
    if (idempotencyKey.length > 255) {
      return c.json({ error: "Idempotency-Key too long (max 255 chars)" }, 400);
    }

    const cacheKey = `idempotency:${c.req.method}:${c.req.path}:${idempotencyKey}`;

    // Check if this request was already processed
    const existing = await redis.get(cacheKey);

    if (existing) {
      const record: IdempotencyRecord = JSON.parse(existing);

      if (record.status === "processing") {
        // Request is currently being processed (concurrent duplicate)
        return c.json(
          { error: "Request is currently being processed", retryAfter: 1 },
          409,
          { "Retry-After": "1" }
        );
      }

      // Return cached response
      c.header("X-Idempotent-Replayed", "true");
      for (const [key, value] of Object.entries(record.headers)) {
        c.header(key, value);
      }
      return c.json(JSON.parse(record.body), record.statusCode as any);
    }

    // Mark as processing (with short TTL in case server crashes)
    const lockRecord: IdempotencyRecord = {
      status: "processing",
      statusCode: 0,
      body: "",
      headers: {},
      createdAt: Date.now(),
    };
    const locked = await redis.set(cacheKey, JSON.stringify(lockRecord), "EX", 30, "NX");

    if (!locked) {
      // Another request grabbed the lock between our GET and SET
      return c.json({ error: "Request is currently being processed" }, 409);
    }

    try {
      // Process the request
      await next();

      // Cache the response
      const responseBody = await getResponseBody(c);
      const completedRecord: IdempotencyRecord = {
        status: "completed",
        statusCode: c.res.status,
        body: responseBody,
        headers: {
          "Content-Type": c.res.headers.get("Content-Type") || "application/json",
        },
        createdAt: Date.now(),
      };

      await redis.set(cacheKey, JSON.stringify(completedRecord), "EX", IDEMPOTENCY_TTL);
    } catch (err) {
      // Remove the lock on failure so retries can proceed
      await redis.del(cacheKey);
      throw err;
    }
  };
}

async function getResponseBody(c: Context): Promise<string> {
  const cloned = c.res.clone();
  return cloned.text();
}

// Database-level idempotency for critical operations
export async function withIdempotency<T>(
  key: string,
  pool: any,
  operation: () => Promise<T>
): Promise<{ result: T; replayed: boolean }> {
  const client = await pool.connect();

  try {
    await client.query("BEGIN");

    // Check if operation was already completed
    const { rows } = await client.query(
      "SELECT result FROM idempotency_keys WHERE key = $1 FOR UPDATE",
      [key]
    );

    if (rows.length > 0) {
      await client.query("COMMIT");
      return { result: JSON.parse(rows[0].result), replayed: true };
    }

    // Execute the operation
    const result = await operation();

    // Store the result
    await client.query(
      "INSERT INTO idempotency_keys (key, result, created_at) VALUES ($1, $2, NOW())",
      [key, JSON.stringify(result)]
    );

    await client.query("COMMIT");
    return { result, replayed: false };
  } catch (err) {
    await client.query("ROLLBACK");
    throw err;
  } finally {
    client.release();
  }
}

Step 2: Apply to Payment Processing

typescript
// src/routes/payments.ts — Idempotent payment processing
import { Hono } from "hono";
import { z } from "zod";
import { idempotency, withIdempotency } from "../middleware/idempotency";
import { pool } from "../db";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const app = new Hono();

app.use("*", idempotency());

const ChargeSchema = z.object({
  orderId: z.string(),
  amount: z.number().positive(),
  currency: z.string().length(3),
  customerId: z.string(),
});

app.post("/payments/charge", async (c) => {
  const body = ChargeSchema.parse(await c.req.json());
  const idempotencyKey = c.req.header("Idempotency-Key")!;

  // Database-level idempotency ensures exactly-once payment
  const { result, replayed } = await withIdempotency(
    `charge:${idempotencyKey}`,
    pool,
    async () => {
      // Check order hasn't been paid already
      const { rows: [order] } = await pool.query(
        "SELECT id, status FROM orders WHERE id = $1",
        [body.orderId]
      );

      if (!order) throw new Error("Order not found");
      if (order.status === "paid") throw new Error("Order already paid");

      // Charge via Stripe (using Stripe's own idempotency)
      const charge = await stripe.paymentIntents.create({
        amount: Math.round(body.amount * 100),
        currency: body.currency,
        customer: body.customerId,
        metadata: { orderId: body.orderId },
      }, {
        idempotencyKey: `stripe:${idempotencyKey}`,
      });

      // Update order status
      await pool.query(
        "UPDATE orders SET status = 'paid', payment_id = $2, paid_at = NOW() WHERE id = $1",
        [body.orderId, charge.id]
      );

      return {
        paymentId: charge.id,
        status: charge.status,
        amount: body.amount,
      };
    }
  );

  if (replayed) {
    c.header("X-Idempotent-Replayed", "true");
  }

  return c.json(result, 201);
});

export default app;

Results

  • Double charges eliminated completely — same Idempotency-Key returns cached result instead of processing again; 47 double charges/month → zero
  • Mobile app retries are safe — automatic retry on timeout sends the same idempotency key; the server returns the original result without reprocessing
  • Concurrent duplicates handled — if two identical requests arrive simultaneously, one processes and the other gets a 409 with Retry-After; no race condition
  • $12K/month in refund costs eliminated — no duplicate payments means no refund requests; customer trust restored
  • Database-level guarantee for payments — Redis handles general API idempotency; critical payment operations use PostgreSQL transactions with FOR UPDATE for stronger consistency