Introduction to API Routes & Route Handlers
Next.js allows you to build full-stack applications by providing a powerful API routing system. With route handlers, you can create RESTful API endpoints directly within your Next.js application without needing a separate backend server. This lesson covers the App Router approach using route.ts files and modern request/response handling.
Note: The App Router (Next.js 13+) uses route handlers in route.ts/route.js files, replacing the older pages/api directory approach from the Pages Router. Route handlers provide better TypeScript support and work seamlessly with React Server Components.
Creating Route Handlers
Route handlers are defined in special route.ts (or route.js) files within the app directory. Each route handler exports functions named after HTTP methods: GET, POST, PUT, DELETE, PATCH, etc.
Basic Route Handler Structure
// app/api/hello/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
return NextResponse.json({ message: 'Hello from Next.js API!' });
}
export async function POST(request: NextRequest) {
const body = await request.json();
return NextResponse.json({ received: body }, { status: 201 });
}
This creates an API endpoint at /api/hello that responds to both GET and POST requests. The endpoint URL matches the file path within the app directory.
HTTP Methods Support
Next.js route handlers support all standard HTTP methods:
// app/api/products/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const productId = params.id;
// Fetch product from database
return NextResponse.json({ id: productId, name: 'Product' });
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const productId = params.id;
const updates = await request.json();
// Update product in database
return NextResponse.json({ id: productId, ...updates });
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const productId = params.id;
// Delete product from database
return NextResponse.json({ deleted: true, id: productId });
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const productId = params.id;
const partialUpdate = await request.json();
// Partial update in database
return NextResponse.json({ id: productId, ...partialUpdate });
}
Working with Request Objects
The NextRequest object extends the standard Web Request API with Next.js-specific features:
Reading Request Data
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
// Get JSON body
const body = await request.json();
// Get URL search params
const searchParams = request.nextUrl.searchParams;
const userId = searchParams.get('userId');
// Get headers
const authorization = request.headers.get('authorization');
// Get cookies
const token = request.cookies.get('auth-token');
return NextResponse.json({
body,
userId,
hasAuth: !!authorization,
hasToken: !!token
});
}
Reading Request Body (Various Formats)
export async function POST(request: NextRequest) {
// JSON
const jsonData = await request.json();
// Form data
const formData = await request.formData();
const name = formData.get('name');
const file = formData.get('file') as File;
// Plain text
const textData = await request.text();
// Binary data
const arrayBuffer = await request.arrayBuffer();
const blob = await request.blob();
return NextResponse.json({ received: true });
}
Warning: You can only read the request body once. After calling request.json(), request.text(), or any other body-reading method, the body stream is consumed and cannot be read again. Store the data in a variable if you need to use it multiple times.
Working with Response Objects
NextResponse provides a powerful API for creating and customizing responses:
JSON Responses
// Basic JSON response
export async function GET() {
return NextResponse.json({ message: 'Success' });
}
// With custom status code
export async function POST() {
return NextResponse.json(
{ error: 'Bad Request' },
{ status: 400 }
);
}
// With custom headers
export async function GET() {
return NextResponse.json(
{ data: [] },
{
status: 200,
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'Custom Value',
'Cache-Control': 'public, max-age=3600'
}
}
);
}
Setting Cookies
export async function POST(request: NextRequest) {
const response = NextResponse.json({ success: true });
// Set a cookie
response.cookies.set('auth-token', 'abc123', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7 // 7 days
});
// Delete a cookie
response.cookies.delete('old-token');
return response;
}
Redirects
import { redirect } from 'next/navigation';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const authenticated = searchParams.get('auth') === 'true';
if (!authenticated) {
redirect('/login');
}
return NextResponse.json({ user: 'data' });
}
// Or using NextResponse.redirect
export async function POST(request: NextRequest) {
// Process form...
return NextResponse.redirect(new URL('/success', request.url));
}
Dynamic Route Parameters
Access dynamic segments from the URL using the params argument:
// app/api/posts/[postId]/comments/[commentId]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { postId: string; commentId: string } }
) {
const { postId, commentId } = params;
return NextResponse.json({
post: postId,
comment: commentId,
data: 'Comment data'
});
}
Streaming Responses
Next.js supports streaming responses for handling large datasets or real-time data:
Using ReadableStream
// app/api/stream/route.ts
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// Send data in chunks
for (let i = 0; i < 10; i++) {
const data = encoder.encode(`Chunk ${i}\n`);
controller.enqueue(data);
// Simulate delay
await new Promise(resolve => setTimeout(resolve, 1000));
}
controller.close();
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/plain',
'Transfer-Encoding': 'chunked'
}
});
}
Server-Sent Events (SSE)
// app/api/events/route.ts
export async function GET(request: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// Send events
const sendEvent = (data: any) => {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
);
};
// Send initial event
sendEvent({ type: 'connected', timestamp: Date.now() });
// Send periodic updates
const interval = setInterval(() => {
sendEvent({ type: 'update', value: Math.random() });
}, 2000);
// Clean up on disconnect
request.signal.addEventListener('abort', () => {
clearInterval(interval);
controller.close();
});
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
});
}
Tip: Server-Sent Events (SSE) are perfect for real-time updates like notifications, live scores, or stock prices. Unlike WebSockets, SSE uses standard HTTP and automatically reconnects if the connection drops.
Error Handling
Implement proper error handling in your route handlers:
// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const userId = parseInt(params.id);
if (isNaN(userId)) {
return NextResponse.json(
{ error: 'Invalid user ID' },
{ status: 400 }
);
}
// Fetch user from database
const user = await fetchUser(userId);
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return NextResponse.json({ user });
} catch (error) {
console.error('Error fetching user:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
CORS Configuration
Configure Cross-Origin Resource Sharing (CORS) for external API access:
// app/api/public/route.ts
export async function GET(request: NextRequest) {
const response = NextResponse.json({ data: 'public' });
// Allow all origins (use cautiously)
response.headers.set('Access-Control-Allow-Origin', '*');
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
return response;
}
// Handle OPTIONS preflight request
export async function OPTIONS(request: NextRequest) {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
});
}
Request Validation
Validate incoming requests using libraries like Zod:
// app/api/users/route.ts
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().int().min(18).max(120)
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Validate request body
const validatedData = userSchema.parse(body);
// Process validated data
const user = await createUser(validatedData);
return NextResponse.json({ user }, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', issues: error.issues },
{ status: 422 }
);
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Route Handler Configuration
Configure route handler behavior using the special export config:
// app/api/data/route.ts
// Opt into dynamic rendering
export const dynamic = 'force-dynamic'; // 'auto' | 'force-dynamic' | 'error' | 'force-static'
// Set runtime
export const runtime = 'edge'; // 'nodejs' | 'edge'
// Configure revalidation
export const revalidate = 60; // Revalidate every 60 seconds
export async function GET() {
return NextResponse.json({ timestamp: Date.now() });
}
Exercise: Create a complete REST API for a blog system with the following endpoints:
- GET /api/posts - List all posts with pagination
- GET /api/posts/[id] - Get a single post
- POST /api/posts - Create a new post (with validation)
- PUT /api/posts/[id] - Update a post
- DELETE /api/posts/[id] - Delete a post
- GET /api/posts/[id]/comments - Get comments for a post
Include proper error handling, request validation, and appropriate HTTP status codes.
Summary
Route handlers in Next.js provide a powerful and flexible way to build API endpoints directly within your application. Key takeaways:
- Use route.ts files in the app directory to define API endpoints
- Export functions named after HTTP methods (GET, POST, PUT, DELETE, etc.)
- NextRequest and NextResponse provide rich APIs for handling requests and responses
- Support for streaming responses and Server-Sent Events for real-time data
- Dynamic route parameters work just like page routes
- Implement proper error handling and request validation
- Configure CORS when needed for external API access
- Use configuration exports to control caching and runtime behavior