echos-ocr/.cursor/rules/api-routes.mdc
Randall Stillwell 9c1aaaa61f Bootstrap agent-context pipeline (L1 + L2 + L3)
Installs a 3-layer Cursor-aligned agent pipeline so future agent
sessions can orient quickly and stay inside guard rails:

L1 — context for any agent reading the repo:
  - AGENTS.md (top-level orientation, conventions, no-go zones)
  - .cursor/rules/ (no-go-zones, api-routes, prisma, prisma-schema-map)
  - .cursor/skills/ (add-api-route, add-prisma-model)
  - docs/SCHEMA_MAP.md generated from prisma/schema.prisma
  - scripts/generate-schema-map.ts (regenerate the map; wired up as
    `npm run schema:map`)

L2 — subagent roles for the 9-stage idea-to-feature pipeline:
  - .cursor/agents/role-*.md (conductor, architect, ia-architect,
    design-system-auditor, implementer, reviewer, ux-reviewer,
    a11y-auditor, doc-writer) with explicit multitask annotations.

L3 — pipeline scaffolding:
  - .github/CODEOWNERS, PR template, and CI workflows (ci.yml,
    preview-smoke.yml, visual-diff.yml, pr-health-rollup.yml).
    Test job is intentionally disabled until Playwright is wired up.
  - .convoys/ folder for per-feature run notes + scripts/log-convoy-event.sh.
  - scripts/wt.sh worktree helper.
  - src/lib/flags/index.ts simple env-driven feature flag wrapper.
  - tests/smoke/app.smoke.spec.ts (Playwright smoke; excluded from
    tsc until @playwright/test is installed — see tsconfig change).

Also writes .agent-context-manifest.yml so the sync-agent-context
skill can detect drift and offer selective updates from upstream.

Follow-ups (not in this commit):
  - Install @playwright/test and re-enable the test job in ci.yml.
  - Review .cursor/agents/role-*.md and trim any roles that don't
    apply to this codebase.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 14:55:18 -05:00

98 lines
3.8 KiB
Text

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