Recipe

Idempotent POST design

Network retries, double-clicks, and queue replays will hit your POST endpoint more than once. Build the contract so the second call is safe by design: same input plus same key yields the same response, without creating duplicate resources. This recipe shows the three-piece pattern Meridian recommends.

1. Require an Idempotency-Key header

Reject any POST that does not carry a client-generatedIdempotency-Keyheader. A UUID v4 per logical user action is the sweet spot: stable across retries of the same action, fresh for a new one. Document the header as required in your OpenAPI spec so SDKs surface it.

2. Cache the response, not just the result

Store the full HTTP response payload keyed by the idempotency key for at least 24 hours. On replay, return that cached payload verbatim with anIdempotent-Replay: trueheader. Clients then know the work was not redone, while still getting an identical body and status code.

3. Lock concurrent retries

If two requests with the same key arrive in flight, the second should wait or return 409. Use a short-lived lock in KV (SETNX with a 30s TTL) around the create step so concurrent retries do not race into double-writes before the cache is populated.

Reference implementation

// app/api/orders/route.ts
import { NextResponse } from 'next/server';
import { kv } from '@vercel/kv';

export async function POST(req: Request) {
  const key = req.headers.get('Idempotency-Key');
  if (!key) {
    return NextResponse.json(
      { error: 'Idempotency-Key header required' },
      { status: 400 }
    );
  }

  const cacheKey = `idem:${key}`;
  const cached = await kv.get(cacheKey);
  if (cached) {
    return NextResponse.json(cached, {
      headers: { 'Idempotent-Replay': 'true' },
    });
  }

  const body = await req.json();
  const order = await createOrder(body);

  // Store result for 24h so retries return the same response.
  await kv.set(cacheKey, order, { ex: 86400 });
  return NextResponse.json(order, { status: 201 });
}