11.4 Route Handlers — NextRequest/NextResponse Types
What are Route Handlers?
API endpoints are defined in route.ts files located in the app/api/ directory.
app/
└── api/
├── users/
│ ├── route.ts # GET /api/users, POST /api/users
│ └── [id]/
│ └── route.ts # GET /api/users/:id, PUT, DELETE
└── auth/
└── route.ts
Basic Route Handler Types
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
// GET /api/users
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const page = parseInt(searchParams.get('page') ?? '1');
const limit = parseInt(searchParams.get('limit') ?? '10');
const users = await getUsers({ page, limit });
return NextResponse.json({
data: users,
page,
limit,
});
}
// POST /api/users
export async function POST(request: NextRequest) {
const body = await request.json();
// body is unknown type
const user = await createUser(body);
return NextResponse.json(user, { status: 201 });
}
Dynamic Route Handlers
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
interface RouteContext {
params: Promise<{ id: string }>;
}
export async function GET(
request: NextRequest,
context: RouteContext
) {
const { id } = await context.params;
const user = await getUserById(id);
if (!user) {
return NextResponse.json(
{ error: 'User not found.' },
{ status: 404 }
);
}
return NextResponse.json(user);
}
export async function PUT(
request: NextRequest,
context: RouteContext
) {
const { id } = await context.params;
const body = await request.json();
const updated = await updateUser(id, body);
return NextResponse.json(updated);
}
export async function DELETE(
request: NextRequest,
context: RouteContext
) {
const { id } = await context.params;
await deleteUser(id);
return new NextResponse(null, { status: 204 });
}
API Response Type Design
// types/api.ts
export interface ApiResponse<T> {
data: T;
message?: string;
}
export interface ApiError {
error: string;
details?: Record<string, string[]>;
code?: string;
}
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
// Type-safe response helpers
function successResponse<T>(data: T, status = 200) {
return NextResponse.json<ApiResponse<T>>({ data }, { status });
}
function errorResponse(error: string, status = 400, details?: Record<string, string[]>) {
return NextResponse.json<ApiError>({ error, details }, { status });
}
function paginatedResponse<T>(
data: T[],
page: number,
limit: number,
total: number
) {
return NextResponse.json<PaginatedResponse<T>>({
data,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
}
Request Validation Pattern
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
published: z.boolean().default(false),
tags: z.array(z.string()).max(10).default([]),
});
type CreatePostInput = z.infer<typeof CreatePostSchema>;
export async function POST(request: NextRequest) {
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ error: 'Invalid JSON format.' },
{ status: 400 }
);
}
const result = CreatePostSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{
error: 'Validation failed',
details: result.error.flatten().fieldErrors,
},
{ status: 422 }
);
}
const post = await createPost(result.data);
return NextResponse.json(post, { status: 201 });
}
Authentication Middleware Pattern
// lib/auth-middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyToken } from './jwt';
type RouteHandler = (
request: NextRequest,
context: { params: Promise<Record<string, string>> }
) => Promise<NextResponse>;
interface AuthenticatedRequest extends NextRequest {
user?: { id: string; role: string };
}
export function withAuth(handler: RouteHandler): RouteHandler {
return async (request, context) => {
const token = request.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
return NextResponse.json(
{ error: 'Authentication required.' },
{ status: 401 }
);
}
try {
const user = await verifyToken(token);
// request is read-only, so add to headers
const newHeaders = new Headers(request.headers);
newHeaders.set('x-user-id', user.id);
newHeaders.set('x-user-role', user.role);
const newRequest = new NextRequest(request, { headers: newHeaders });
return handler(newRequest, context);
} catch {
return NextResponse.json(
{ error: 'Invalid token.' },
{ status: 401 }
);
}
};
}
// Usage
export const GET = withAuth(async (request, { params }) => {
const userId = request.headers.get('x-user-id')!;
const data = await getUserData(userId);
return NextResponse.json(data);
});
Useful NextResponse Methods
// JSON response
NextResponse.json({ data: 'value' }, { status: 200 });
// Redirect
NextResponse.redirect(new URL('/login', request.url));
// Rewrite
NextResponse.rewrite(new URL('/new-path', request.url));
// Set headers
const response = NextResponse.json(data);
response.headers.set('Cache-Control', 'no-store');
response.headers.set('X-Custom-Header', 'value');
// Set cookies
response.cookies.set({
name: 'session',
value: token,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
});
// Empty response (204 No Content)
return new NextResponse(null, { status: 204 });
Pro Tips
1. CORS handling
const CORS_HEADERS = {
'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN ?? '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};
export async function OPTIONS() {
return NextResponse.json({}, { headers: CORS_HEADERS });
}
export async function GET(request: NextRequest) {
const data = await fetchData();
return NextResponse.json(data, { headers: CORS_HEADERS });
}
2. Streaming responses
export async function GET() {
const stream = new TransformStream();
const writer = stream.writable.getWriter();
const encoder = new TextEncoder();
// Async data streaming
(async () => {
for await (const chunk of getLargeData()) {
await writer.write(encoder.encode(JSON.stringify(chunk) + '\n'));
}
await writer.close();
})();
return new Response(stream.readable, {
headers: { 'Content-Type': 'application/x-ndjson' },
});
}