Four hard rules applied to every API route and page. These are non-negotiable — the AI follows them automatically when generating code.
Every API route that accepts a request body must validate it with Zod before use. Never trust request.json() directly.
✓ correct
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
message: z.string().min(1).max(1000),
})
export async function POST(req: Request) {
const body = await req.json()
const parsed = schema.safeParse(body)
if (!parsed.success) {
return Response.json({ error: parsed.error.flatten() }, { status: 400 })
}
// safe to use parsed.data
}✗ avoid
export async function POST(req: Request) {
const { email, message } = await req.json() // ❌ never do this
}Any route or page that requires login must check the session at the top and return 401 immediately if there is none. Do not rely on middleware alone.
✓ correct
import { auth } from '@/lib/auth'
import { headers } from 'next/headers'
export async function GET() {
const session = await auth?.api.getSession({ headers: await headers() })
if (!session) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
// session.user is now available
}✗ avoid
export async function GET() {
// ❌ assumes middleware handles auth — it doesn't always fire
const data = await getSecretData()
return Response.json(data)
}API keys, tokens, and passwords go in .env only. Reference them via process.env. Never hardcode them, log them, or expose them in client-side code.
✓ correct
// .env
EXTERNAL_API_KEY=sk-...
// lib/integrations/myservice.ts
const API_KEY = process.env.EXTERNAL_API_KEY ?? ''✗ avoid
// ❌ never hardcode secrets
const API_KEY = 'sk-abc123...'
// ❌ never expose in client components (no 'use client' + no NEXT_PUBLIC_ for secrets)
const API_KEY = process.env.NEXT_PUBLIC_SECRET_KEYData from external APIs, webhooks, and form submissions must be validated before use — not just request bodies.
✓ correct
import { z } from 'zod'
const PersonioEmployeeSchema = z.object({
id: z.number(),
attributes: z.object({
first_name: z.object({ value: z.string() }),
email: z.object({ value: z.string().email() }),
}),
})
const res = await personioFetch('/company/employees')
const raw = await res.json()
const employee = PersonioEmployeeSchema.parse(raw.data[0])✗ avoid
const res = await personioFetch('/company/employees')
const { data } = await res.json()
const email = data[0].attributes.email.value // ❌ no validation