Files
community-rule/.cursor/rules/api-routes.mdc
T
2026-05-22 15:50:33 -06:00

86 lines
3.2 KiB
Plaintext

---
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.