Next.js Route Handlers Overview
Route Handlers allow you to create API endpoints using the Web Request and Response APIs. They're defined in route.ts files within the app directory.
Basic Structure File Convention
Route handlers use route.ts (or route.js):
app/ ├── api/ │ ├── users/ │ │ └── route.ts # /api/users │ └── posts/ │ ├── route.ts # /api/posts │ └── [id]/ │ └── route.ts # /api/posts/:id
HTTP Methods
Export functions named after HTTP methods:
// app/api/users/route.ts import { NextResponse } from 'next/server'
export async function GET() { const users = await db.user.findMany() return NextResponse.json(users) }
export async function POST(request: Request) { const body = await request.json() const user = await db.user.create({ data: body }) return NextResponse.json(user, { status: 201 }) }
Supported methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
Request Handling Reading Request Body export async function POST(request: Request) { // JSON body const json = await request.json()
// Form data const formData = await request.formData() const name = formData.get('name')
// Text body const text = await request.text()
return NextResponse.json({ received: true }) }
URL Parameters
Dynamic route parameters:
// app/api/posts/[id]/route.ts interface RouteContext { params: Promise<{ id: string }> }
export async function GET( request: Request, context: RouteContext ) { const { id } = await context.params const post = await db.post.findUnique({ where: { id } })
if (!post) { return NextResponse.json( { error: 'Not found' }, { status: 404 } ) }
return NextResponse.json(post) }
Query Parameters export async function GET(request: Request) { const { searchParams } = new URL(request.url) const page = searchParams.get('page') ?? '1' const limit = searchParams.get('limit') ?? '10'
const posts = await db.post.findMany({ skip: (parseInt(page) - 1) * parseInt(limit), take: parseInt(limit), })
return NextResponse.json(posts) }
Request Headers export async function GET(request: Request) { const authHeader = request.headers.get('authorization')
if (!authHeader?.startsWith('Bearer ')) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ) }
const token = authHeader.split(' ')[1] // Validate token...
return NextResponse.json({ authenticated: true }) }
Response Handling JSON Response import { NextResponse } from 'next/server'
export async function GET() { return NextResponse.json( { message: 'Hello' }, { status: 200 } ) }
Setting Headers export async function GET() { return NextResponse.json( { data: 'value' }, { headers: { 'Cache-Control': 'max-age=3600', 'X-Custom-Header': 'custom-value', }, } ) }
Setting Cookies import { cookies } from 'next/headers'
export async function POST(request: Request) { const cookieStore = await cookies()
// Set cookie cookieStore.set('session', 'abc123', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 24 * 7, // 1 week })
return NextResponse.json({ success: true }) }
Redirects import { redirect } from 'next/navigation' import { NextResponse } from 'next/server'
export async function GET() { // Option 1: redirect function (throws) redirect('/login')
// Option 2: NextResponse.redirect return NextResponse.redirect(new URL('/login', request.url)) }
Streaming Responses
Text Streaming
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
controller.enqueue(encoder.encode(data: ${i}\n\n))
await new Promise(resolve => setTimeout(resolve, 100))
}
controller.close()
},
})
return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }, }) }
AI/LLM Streaming export async function POST(request: Request) { const { prompt } = await request.json()
const response = await openai.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: prompt }], stream: true, })
const stream = new ReadableStream({ async start(controller) { for await (const chunk of response) { const text = chunk.choices[0]?.delta?.content || '' controller.enqueue(new TextEncoder().encode(text)) } controller.close() }, })
return new Response(stream, { headers: { 'Content-Type': 'text/plain' }, }) }
CORS Configuration export async function OPTIONS() { return new Response(null, { status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, }) }
export async function GET() { return NextResponse.json( { data: 'value' }, { headers: { 'Access-Control-Allow-Origin': '*', }, } ) }
Caching Static (Default for GET) // Cached by default export async function GET() { const data = await fetch('https://api.example.com/data') return NextResponse.json(await data.json()) }
Opt-out of Caching export const dynamic = 'force-dynamic'
export async function GET() { // Always fresh }
// Or use cookies/headers (auto opts out) import { cookies } from 'next/headers'
export async function GET() { const cookieStore = await cookies() // Now dynamic }
Error Handling export async function GET(request: Request) { try { const data = await riskyOperation() return NextResponse.json(data) } catch (error) { console.error('API Error:', error)
if (error instanceof ValidationError) {
return NextResponse.json(
{ error: error.message },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
)
} }
Resources
For detailed patterns, see:
references/http-methods.md - Complete HTTP method guide references/streaming-responses.md - Advanced streaming patterns examples/crud-api.md - Full CRUD API example