When working on APIs:
- Always build resource-oriented, RESTful APIs
- Never trust user input
- Use Zod for all validation
- Define input and output types with Zod schemas
- Export inferred types generated from schemas
Design & HTTP semantics
- Prefer clear, resource-based routes:
GET /api/projects – list
POST /api/projects – create
GET /api/projects/:id – detail
PATCH /api/projects/:id – partial update
- Use appropriate status codes:
200/201 success, 400 validation errors, 401 unauthenticated, 403 forbidden, 404 not found, 409 conflict, 500 unexpected.
- Keep responses stable and versioned:
- Avoid breaking changes; if needed, version:
/api/v1/....
Validation & Types (Zod-first)
- All external input (body, query, params, headers) MUST:
- Be parsed and validated with Zod.
- Return
400 with a safe error payload on validation failure.
- Always define a schema + inferred TypeScript type:
import { z } from "zod";
export const createProjectSchema = z.object({
name: z.string().min(1),
description: z.string().optional(),
});
export type CreateProjectInput = z.infer<typeof createProjectSchema>;
Canonical Next.js route pattern (simplified)
// app/api/projects/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
const querySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
pageSize: z.coerce.number().int().min(1).max(100).default(20),
});
export async function GET(req: Request) {
const url = new URL(req.url);
const parseResult = querySchema.safeParse(
Object.fromEntries(url.searchParams),
);
if (!parseResult.success) {
return NextResponse.json({ error: "Invalid query" }, { status: 400 });
}
const { page, pageSize } = parseResult.data;
// Fetch data, always with LIMIT and stable ordering
return NextResponse.json({ data: [], page, pageSize });
}
Security must-dos
- Auth & authz:
- Require authentication for any non-public data.
- Authorize at the resource level (e.g. filter by
user_id / business_id on every query).
- Input safety:
- Never pass unvalidated input into DB queries or third-party APIs.
- Error handling:
- Do not leak stack traces or internal error details; log them server-side only.
- Rate limiting:
- Apply rate limits on sensitive routes (login, write-heavy operations).
Performance & pagination
- Always paginate list endpoints:
- Use cursor or page/pageSize and stable ordering (e.g. by
created_at or id).
- Prefer single, well-scoped queries over N+1 patterns.
- Return only required fields; avoid over-fetching.
Canonical pagination shape:
{
data: T[];
nextCursor?: string;
}
1---2name: apis3description: Editing API routes4---56When working on APIs:78- Always build **resource-oriented, RESTful** APIs9- Never trust user input10- Use **Zod** for all validation11- Define **input and output** types with Zod schemas12- Export **inferred types** generated from schemas1314## Design & HTTP semantics1516- Prefer **clear, resource-based routes**:17 - `GET /api/projects` – list18 - `POST /api/projects` – create19 - `GET /api/projects/:id` – detail20 - `PATCH /api/projects/:id` – partial update21- Use **appropriate status codes**:22 - `200/201` success, `400` validation errors, `401` unauthenticated, `403` forbidden, `404` not found, `409` conflict, `500` unexpected.23- Keep responses **stable and versioned**:24 - Avoid breaking changes; if needed, version: `/api/v1/...`.2526## Validation & Types (Zod-first)2728- All external input (body, query, params, headers) MUST:29 - Be parsed and validated with **Zod**.30 - Return `400` with a safe error payload on validation failure.31- Always define a **schema + inferred TypeScript type**:3233```ts34import { z } from "zod";3536export const createProjectSchema = z.object({37 name: z.string().min(1),38 description: z.string().optional(),39});4041export type CreateProjectInput = z.infer<typeof createProjectSchema>;42```4344## Canonical Next.js route pattern (simplified)4546```ts47// app/api/projects/route.ts48import { NextResponse } from "next/server";49import { z } from "zod";5051const querySchema = z.object({52 page: z.coerce.number().int().min(1).default(1),53 pageSize: z.coerce.number().int().min(1).max(100).default(20),54});5556export async function GET(req: Request) {57 const url = new URL(req.url);58 const parseResult = querySchema.safeParse(59 Object.fromEntries(url.searchParams),60 );61 if (!parseResult.success) {62 return NextResponse.json({ error: "Invalid query" }, { status: 400 });63 }6465 const { page, pageSize } = parseResult.data;66 // Fetch data, always with LIMIT and stable ordering67 return NextResponse.json({ data: [], page, pageSize });68}69```7071## Security must-dos7273- **Auth & authz**:74 - Require authentication for any non-public data.75 - Authorize at the **resource level** (e.g. filter by `user_id` / `business_id` on every query).76- **Input safety**:77 - Never pass unvalidated input into DB queries or third-party APIs.78- **Error handling**:79 - Do not leak stack traces or internal error details; log them server-side only.80- **Rate limiting**:81 - Apply rate limits on sensitive routes (login, write-heavy operations).8283## Performance & pagination8485- Always paginate list endpoints:86 - Use **cursor or page/pageSize** and **stable ordering** (e.g. by `created_at` or `id`).87- Prefer **single, well-scoped queries** over N+1 patterns.88- Return only required fields; avoid over-fetching.8990Canonical pagination shape:9192```ts93{94 data: T[];95 nextCursor?: string;96}97```