Defines conventions for Next.js 16 API route handlers, server actions, middleware chaining, Zod validation, error handling, and typed responses. Trigger when the user mentions: "API route", "route handler", "REST endpoint", "server action", "API middleware", "request validation", "API error handling", "NextResponse", "POST handler", "GET handler", "API versioning", "webhook", or asks to build any backend endpoint or server-side mutation.
Conventions for route handlers (app/api/) and server actions (lib/actions/).
src/app/api/
├── auth/[...nextauth]/route.ts # NextAuth — do not modify
├── v1/
│ ├── users/
│ │ ├── route.ts # GET /api/v1/users, POST /api/v1/users
│ │ └── [id]/
│ │ └── route.ts # GET /api/v1/users/:id, PATCH, DELETE
│ ├── reports/
│ │ └── route.ts
│ └── settings/
│ └── route.ts
// src/app/api/v1/users/route.ts
import { NextResponse, type NextRequest } from 'next/server';
import { z } from 'zod';
import { requireRole } from '@/lib/auth/session';
import { apiError, apiSuccess } from '@/lib/api/response';
import { getUsers, createUser } from '@/lib/db/queries/users';
// GET /api/v1/users
export async function GET(req: NextRequest) {
try {
const session = await requireRole('admin', 'ops');
const { searchParams } = new URL(req.url);
const page = Number(searchParams.get('page') ?? 1);
const limit = Number(searchParams.get('limit') ?? 20);
const users = await getUsers({ page, limit });
return apiSuccess(users);
} catch (err) {
return apiError(err);
}
}
// POST /api/v1/users
const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
role: z.enum(['admin', 'ops', 'superuser']),
});
export async function POST(req: NextRequest) {
try {
await requireRole('admin');
const body = await req.json();
const parsed = createUserSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: 'Validation failed', issues: parsed.error.flatten() }, { status: 422 });
}
const user = await createUser(parsed.data);
return apiSuccess(user, 201);
} catch (err) {
return apiError(err);
}
}
// src/lib/api/response.ts
import { NextResponse } from 'next/server';
export function apiSuccess<T>(data: T, status = 200) {
return NextResponse.json({ data }, { status });
}
export function apiError(err: unknown, status?: number) {
if (err instanceof Response) return err; // re-throw NextAuth redirects
const message = err instanceof Error ? err.message : 'Internal server error';
const code = status ?? (message === 'Unauthorized' ? 401 : message === 'Forbidden' ? 403 : 500);
console.error('[API Error]', message, err);
return NextResponse.json({ error: message }, { status: code });
}
// src/lib/api/middleware.ts
import { type NextRequest, NextResponse } from 'next/server';
import type { Role } from '@/types/auth';
import { requireRole } from '@/lib/auth/session';
type Handler = (req: NextRequest, context?: any) => Promise<NextResponse>;
export function withRole(...roles: Role[]) {
return (handler: Handler): Handler =>
async (req, ctx) => {
try {
await requireRole(...roles);
return handler(req, ctx);
} catch {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
};
}
export function withValidation<T>(schema: { safeParse: (v: unknown) => any }) {
return (handler: (req: NextRequest, data: T) => Promise<NextResponse>): Handler =>
async (req, ctx) => {
const body = await req.json().catch(() => ({}));
const parsed = schema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: 'Validation failed', issues: parsed.error.flatten() }, { status: 422 });
}
return handler(req, parsed.data);
};
}
Usage:
export const POST = withRole('admin')(
withValidation(createUserSchema)(
async (req, data) => {
const user = await createUser(data);
return apiSuccess(user, 201);
}
)
);
// src/lib/actions/users.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
import { requireRole } from '@/lib/auth/session';
import { db } from '@/lib/db/client';
import { users } from '@/lib/db/schema/users';
const updateUserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(2).optional(),
role: z.enum(['admin', 'ops', 'superuser']).optional(),
});
export async function updateUser(input: z.infer<typeof updateUserSchema>) {
await requireRole('admin');
const parsed = updateUserSchema.safeParse(input);
if (!parsed.success) throw new Error('Invalid input');
await db.update(users).set(parsed.data).where(eq(users.id, parsed.data.id));
revalidatePath('/admin/users');
}
export async function deleteUser(id: string) {
await requireRole('admin');
await db.delete(users).where(eq(users.id, id));
revalidatePath('/admin/users');
}
requireRole() before touching the database.req.json().apiSuccess / apiError helpers consistently.try/catch and delegate to apiError().console.error('[API Error]', context, err).