Skip to main content
Advertisement

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' },
});
}
Advertisement