Overview
All data flows through versioned REST endpoints under /api/v1/. Routes return consistent response envelopes, validate inputs with Zod, and use typed error handling.
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
- Consistent response envelope pattern
- Zod schema validation for all inputs
- Typed error classes with proper HTTP status codes
- Composable middleware for auth, logging, CORS
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 { validateRequest } from '@/lib/validation';
import { handleError, NotFoundError } from '@/lib/errors';
import { logger } from '@/lib/logger';
import { z } from 'zod';
const GetItemsSchema = z.object({
limit: z.coerce.number().min(1).max(100).default(20),
});
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const validation = validateRequest(
Object.fromEntries(searchParams),
GetItemsSchema
);
if (!validation.success) return validation.error;
const { limit } = validation.data;
const items = await getItems(limit); // Your data fetching
return NextResponse.json({ ok: true, data: items });
} catch (error) {
return handleError(error);
}
}Tip: See the full working example at /examples/api/example-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
const CreateItemSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().optional(),
});
export async function POST(request: Request) {
try {
const body = await request.json();
const validation = validateRequest(body, CreateItemSchema);
if (!validation.success) return validation.error;
const { name, description } = validation.data;
const item = await createItem({ name, description });
logger.info('Item created', { itemId: item.id });
return NextResponse.json(
{ ok: true, data: item },
{ status: 201 }
);
} catch (error) {
return handleError(error);
}
}Middleware Composition
Combining auth, logging, and rate limiting.
// Composing middleware in a route
import { withAuth } from '@/lib/middleware/auth';
import { withRequestLogging } from '@/lib/middleware/logging';
export async function GET(request: Request) {
// Check authentication
const auth = await withAuth(request);
if (!auth.session) {
return NextResponse.json(
{ ok: false, error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
{ status: 401 }
);
}
// Log the request
withRequestLogging(request, auth.user);
// Your handler logic...
}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
const { email } = await request.json();
await createUser({ email }); // Dangerous!Do
// Validating with Zod
const validation = validateRequest(body, schema);
if (!validation.success) return validation.error;
const { email } = validation.data; // Type-safe- Using different error formats across routes
- Missing request logging for debugging