Overview
All data flows through versioned REST endpoints under /api/v1/. Routes export a spec object that drives OpenAPI documentation (/admin/api-docs) and automatic input validation.
What it is
Next.js App Router API routes with standardized patterns for request handling, response formatting, and error management.
Why we use it
Consistency across all endpoints, type safety, predictable error handling, and clear separation of concerns.
When to use
Any server-side data operation that needs to be accessible from client components or external systems.
Key Features
- Spec-based validation - define once, validate automatically
- OpenAPI docs auto-generated from spec schemas
- Consistent response envelope pattern
- withApiLogging + withAuth HOF composition pattern
Quick Start
Minimal API Route
A complete GET endpoint with validation and error handling.
// src/app/api/v1/items/route.ts
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { withApiLogging } from '@/lib/api-logging';
import { withAuth } from '@/lib/api-auth';
import type { RouteSpec } from '@/lib/api-docs';
// 1. Define spec for OpenAPI docs + auto-validation
export const spec = {
GET: {
summary: 'List items',
tags: ['Items'],
queryParams: z.object({
limit: z.coerce.number().min(1).max(100).default(20),
}),
response: z.array(itemSchema),
requiresAuth: true,
},
} satisfies RouteSpec;
// 2. Handler receives pre-validated query/body
export const GET = withApiLogging(
withAuth({ scope: 'project' }, spec.GET, async (_request, { query }) => {
const { limit } = query; // Already validated!
const items = await getItems(limit);
return NextResponse.json({ ok: true, data: items });
})
);Tip: See working examples at src/app/api/v1/programs/route.ts or src/app/api/v1/files/route.ts
Patterns
Response Envelope
All API responses use this format for consistency.
// Success response
{ ok: true, data: T }
// Error response
{
ok: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Email is required'
}
}POST Route with Validation
Creating resources with input validation.
// POST with body validation via spec
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { withApiLogging } from '@/lib/api-logging';
import { withAuth } from '@/lib/api-auth';
import { logger } from '@/lib/logger';
import type { RouteSpec } from '@/lib/api-docs';
export const spec = {
POST: {
summary: 'Create item',
tags: ['Items'],
requestBody: z.object({
name: z.string().min(1).max(100),
description: z.string().optional(),
}),
response: itemSchema,
requiresAuth: true,
},
} satisfies RouteSpec;
export const POST = withApiLogging(
withAuth({ scope: 'project' }, spec.POST, async (_request, { body }) => {
const { name, description } = body; // Pre-validated!
const item = await createItem({ name, description });
logger.info('Item created', { itemId: item.id });
return NextResponse.json({ ok: true, data: item }, { status: 201 });
})
);Middleware Composition
Combining auth, logging, and rate limiting.
// Composing middleware with spec-based validation
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { withApiLogging } from '@/lib/api-logging';
import { withAuth } from '@/lib/api-auth';
import type { RouteSpec } from '@/lib/api-docs';
// withApiLogging: Request logging, rate limiting, RLS context
// withAuth: Authentication + authorization + auto-validation
export const spec = {
GET: {
summary: 'Get project items',
tags: ['Items'],
queryParams: z.object({ search: z.string().optional() }),
response: z.array(itemSchema),
requiresAuth: true,
},
} satisfies RouteSpec;
export const GET = withApiLogging(
withAuth(
{ scope: 'project', requiredAccess: 'read' },
spec.GET, // Pass spec for auto-validation
async (_request, { auth, actor, projectId, query }) => {
// auth: { userId, apiKey, type: 'session' | 'api_key' }
// actor: for audit logging
// projectId: extracted from route params
// query: pre-validated via spec.GET.queryParams
return NextResponse.json({ ok: true, data: {} });
}
)
);Watch Out
Returning raw data without envelope wrapper
Don't
// Returning raw data return NextResponse.json(items);
Do
// Using response envelope
return NextResponse.json({
ok: true,
data: items
});Trusting request body without Zod validation
Don't
// Trusting request body without spec
const { email } = await request.json();
await createUser({ email }); // No validation!Do
// Let spec handle validation automatically
withAuth({ scope: 'user' }, spec.POST, async (_req, { body }) => {
const { email } = body; // Pre-validated via spec!
});- Using different error formats across routes
- Missing request logging for debugging