feat(api): generic Idempotency-Key support for safe POST retries

Introduces `withIdempotency()` in `src/lib/idempotency.ts` — wraps a
mutation handler so that a POST/PUT/PATCH/DELETE carrying an
`Idempotency-Key` header is replayed from the cached response instead of
running twice. Cache row is keyed by `(userId, key, method, path)` with
24h TTL; replays carry `X-Idempotent-Replay: true` so callers can
distinguish them.

Applied to the three mutation endpoints most exposed to client retries:
  • POST `/api/measurements`
  • POST `/api/mood-entries`
  • POST `/api/medications/[id]/intake`

Without the header: zero behaviour change. Existing clients that don't
send the header simply skip the cache lookup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-03 21:53:02 +02:00
1 parent c8387d4f2b
commit 9766cedea4
5 files changed
+338 -6

No files matched your search

+13 -2
View File
@@ -9,6 +9,8 @@ import {
listMeasurementsSchema,
getUnitForType,
} from "@/lib/validations/measurement";
import { withIdempotency } from "@/lib/idempotency";
import { getSession } from "@/lib/auth/session";
import { NextRequest } from "next/server";
import type {
MeasurementType,
@@ -17,6 +19,11 @@ import type {
} from "@/generated/prisma/client";
import { Prisma } from "@/generated/prisma/client";
async function resolveUserIdForIdempotency(): Promise<string | null> {
const session = await getSession().catch(() => null);
return session?.user.id ?? null;
}
export const GET = apiHandler(async (request: NextRequest) => {
const { user } = await requireAuth();
@@ -59,7 +66,11 @@ export const GET = apiHandler(async (request: NextRequest) => {
});
});
export const POST = apiHandler(async (request: NextRequest) => {
export const POST = apiHandler(
withIdempotency<[NextRequest]>(postMeasurement, resolveUserIdForIdempotency),
);
async function postMeasurement(request: NextRequest) {
const { user } = await requireAuth();
const { data: body, error: jsonError } = await safeJson(request);
@@ -159,4 +170,4 @@ export const POST = apiHandler(async (request: NextRequest) => {
});
return apiSuccess(measurement, 201);
});
}