---
description: Conventions for Next.js App Router API route handlers in this repo
globs: src/app/api/**/route.ts
---

# API Route Conventions

Every `src/app/api/**/route.ts` follows the same shape: auth → parse body → query Prisma scoped to `orgId` → return JSON, all wrapped in `try / handleApiError`.

## Authentication & Authorization

- Import from `@/lib/api-auth`:

```ts
import { NextRequest, NextResponse } from "next/server";
import { requireApiAuthWithOrg, handleApiError } from "@/lib/api-auth";
import { prisma } from "@/lib/db";

export async function GET(request: NextRequest) {
  try {
    const session = await requireApiAuthWithOrg();
    // session.user.id, session.user.orgId, session.user.role
    // ...
  } catch (error) {
    return handleApiError(error);
  }
}
```

- `requireApiAuthWithOrg()` returns an `OrgSession` (extends NextAuth `Session` with `user.id` and `user.orgId`) — both are always defined after this call.
- Pass an optional `Action` to enforce a permission in one line: `await requireApiAuthWithOrg("cards.delete")`. Throws `PermissionError`; `handleApiError` returns 403.
- For routes that don't need org scoping (rare — typically auth callbacks), use `requireApiAuth()` instead.

## Multi-tenancy is mandatory

Every Prisma query against an org-scoped model (`ResponseCard`, `FormTemplate`, `Person`, `Integration`, …) MUST scope by `organizationId`:

```ts
const card = await prisma.responseCard.findUnique({ where: { id } });
if (!card || card.organizationId !== session.user.orgId) {
  return NextResponse.json({ error: "Card not found" }, { status: 404 });
}
```

For lists: include `organizationId: session.user.orgId` in the `where` filter directly. Returning a 404 (not 403) on cross-org access is the convention so we don't leak existence.

## Request validation

- Body parsing is hand-rolled today. Use the defensive pattern from `src/app/api/cards/[id]/route.ts`:

```ts
const body = await request.json().catch(() => ({}));
const data: Record<string, unknown> = {};
const stringFields = ["name", "email", /* ... */];
for (const field of stringFields) {
  if (body[field] != null) data[field] = String(body[field]);
}
```

- Zod is in `package.json` but not widely used. If you reach for it in a new route, that's fine — just stay consistent within the route.

## Error handling

- Wrap every handler in `try { ... } catch (error) { return handleApiError(error); }`. Never throw to the framework.
- `handleApiError` returns 401 for `ApiAuthError`, 403 for `PermissionError`, 500 (with `console.error`) for anything else.
- For domain errors that aren't auth/permission, return `NextResponse.json({ error: "..." }, { status: 4xx })` directly — don't invent new error classes for one-off cases.

## Database access

- Always `import { prisma } from "@/lib/db";` — the lazy `Proxy` singleton. Never `new PrismaClient()`.
- Use `select` or `include` only when you need it; the default fetch is fine for small models.
- For writes, prefer `update`/`create` over `upsert` unless you actually need both paths.

## Response shape

- Success collections: `NextResponse.json({ items, total, page, limit })` (see `src/app/api/cards/route.ts` GET).
- Success single: `NextResponse.json(record)` (no envelope).
- Created: `NextResponse.json(record, { status: 201 })`.
- Errors: `{ error: string, action?: string }` — `handleApiError` already does this.

## Dynamic routes

Next.js 16 dynamic route params are async. Use:

```ts
export async function GET(
  _request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  // ...
}
```

## After adding / changing a route

- Update the route table in `README.md` if it's a public-shape change.
- If the route writes to `ResponseCard.firstName` or `lastName`, recompute `name` (see the canonical recompute block in `src/app/api/cards/[id]/route.ts`).
