| name | api-patterns |
| description | REST API structure, error handling, and response shapes for Next.js App Router routes. Loaded when writing or reviewing API route handlers. |
API Patterns
Route structure
Routes follow: auth → parse → validate → service → respond
export async function POST(req: Request) {
try {
const { id: userId } = getUserFromRequest(req);
const body = await req.json();
const parsed = schema.safeParse(body);
if (!parsed.success) {
return Response.json({ error: parsed.error.flatten() }, { status: 400 });
}
const result = await someService.method({ ...parsed.data, userId });
return Response.json(result, { status: 201 });
} catch (error: unknown) {
return handleError(error);
}
}
- Routes are thin — no business logic, no DB queries
- Always import from module barrel (
@/modules/name)
Response shapes
Routes return data directly — no { data: T } wrapper:
return Response.json(post, { status: 200 });
return Response.json(posts, { status: 200 });
return Response.json(post, { status: 201 });
return new Response(null, { status: 204 });
return Response.json({ error: parsed.error.flatten() }, { status: 400 });
Error responses
handleError in lib/errors/ maps HttpError instances to JSON responses:
throw new HttpError(404, 'Post not found');
throw new HttpError(403, 'Forbidden');
throw new HttpError(409, 'Slug already exists');
} catch (error: unknown) {
return handleError(error);
}
| Status | Meaning |
|---|
| 400 | Validation error (Zod) |
| 401 | Unauthenticated (thrown by getUserFromRequest) |
| 403 | Wrong owner / forbidden |
| 404 | Resource not found |
| 409 | Conflict (duplicate) |
| 500 | Unexpected (caught by handleError) |
Rules
- Never trust client-provided IDs for ownership — verify in DB via service
- Always use
safeParse — never parse (which throws)
- Always type catch as
unknown
- Never access
req.json() before auth check