--- description: App Router API handler conventions (Next.js + Prisma + Zod) globs: app/api/**/*.ts,lib/server/**/*.ts alwaysApply: false --- # API route anatomy Every DB-touching handler in `app/api/**/route.ts` follows the same skeleton. Keep new routes within this shape so auth, config, and validation stay uniform. 1. **Config guard (first line of the handler).** ```typescript if (!isDatabaseConfigured()) return dbUnavailable(); ``` From `lib/server/env` + `lib/server/responses`. Returns a consistent 503 when `CLOUDRON_POSTGRESQL_URL` is missing (local dev, preview builds). 2. **Auth (when the route requires a user).** ```typescript const user = await getSessionUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } ``` From `lib/server/session`. Never read session cookies or tokens directly. 3. **Body parsing + validation (POST/PUT/PATCH).** ```typescript const parsed = await readLimitedJson(request); const result = mySchema.safeParse(parsed); if (!result.success) return jsonFromZodError(result.error); ``` Helpers live in `lib/server/validation/{requestBody,zodHttp}.ts`. All payload schemas belong in `lib/server/validation/*.ts` (today: `createFlowSchemas.ts`) — colocate new schemas there rather than inline in the route. 4. **Prisma access** via `import { prisma } from "lib/server/db"`. Do not instantiate `PrismaClient` directly. 5. **Responses** via `NextResponse.json(...)`. Shared shapes (`dbUnavailable`, `unauthorized`, `notFound`, `rateLimited`, `serverMisconfigured`, `internalError`) and the generic `errorJson(code, message, status, opts?)` live in `lib/server/responses.ts`. Add new shared responses there when a pattern repeats in two routes. 6. **Errors + observability.** All 4xx/5xx bodies use the canonical shape `{ error: { code, message }, details? }` with codes from the `ApiErrorCode` union in `lib/server/responses.ts`. Wrap handlers with `apiRoute("scope.name", async (req, ctx, { requestId }) => { ... })` from `lib/server/apiRoute.ts` so an `x-request-id` is generated / forwarded onto every response and uncaught throws return a canonical 500 with the id logged via `lib/logger`. # Server-only isolation `lib/server/*` is the server boundary. Anything that: - imports `@prisma/client`, - reads secrets from `env`, - sends email, hashes tokens, or touches sessions …lives under `lib/server/`. Never import `lib/server/*` from client components, `app/components/**`, or any file marked `"use client"`. Shared logic safe for both sides goes in `lib/*`. # Deferred — follow existing code, don't invent These areas are still settling. Match whatever the nearest route already does instead of introducing new patterns: - **Rate limiting.** `lib/server/rateLimit.ts` is an in-memory stopgap marked for replacement. Reuse `rateLimitKey()` where limiting is needed; don't design a new limiter. When returning 429, prefer `rateLimited(retryAfterMs)` from `responses.ts` so the body and `Retry-After` header stay uniform. - **Pagination / filtering.** Only `rules/route.ts` paginates (`take` capped at 100). Mirror it if you add list endpoints; don't invent cursors or offset contracts unilaterally.