Skip to Content
Grab N GoNext.js API Routes

Next.js API Routes

Targeting Next.js 16 (App Router — Route Handlers)

Syntax

Route Handler
// app/api/hello/route.ts import { NextResponse } from 'next/server' export async function GET() { return NextResponse.json({ message: 'Hello' }) } // GET /api/hello → { "message": "Hello" }
HTTP Methods
// app/api/users/route.ts export async function GET() { /* list */ } export async function POST() { /* create */ } export async function PUT() { /* replace */ } export async function PATCH() { /* update */ } export async function DELETE() { /* remove */ } export async function HEAD() { /* headers only */ } export async function OPTIONS() { /* CORS preflight */ }
Request Object
export async function POST(request: Request) { request.method // 'POST' request.url // full URL string request.headers // Headers object request.json() // parse JSON body request.text() // raw text body request.formData() // parse form data request.arrayBuffer() // binary data }
NextRequest Extras
import { NextRequest } from 'next/server' export async function GET(req: NextRequest) { req.nextUrl.pathname // '/api/users' req.nextUrl.searchParams.get('q') // query param req.cookies.get('token') // cookie value req.headers.get('x-forwarded-for') // client IP }
Response Helpers
import { NextResponse } from 'next/server' NextResponse.json(data) // JSON NextResponse.json(data, { status: 201 }) // with status NextResponse.redirect(new URL('/login', req.url)) NextResponse.next() // continue (proxy) // plain Response works too new Response('OK', { status: 200 }) new Response(null, { status: 204 }) // no content
Dynamic Params
// app/api/users/[id]/route.ts interface Ctx { params: Promise<{ id: string }> } export async function GET(req: Request, ctx: Ctx) { const { id } = await ctx.params return NextResponse.json({ id }) } // GET /api/users/42 → { "id": "42" }

Beginner Patterns

JSON Body
export async function POST(req: Request) { const body = await req.json() const user = await db.user.create({ data: body }) return NextResponse.json(user, { status: 201 }) }
Query Params
import { NextRequest } from 'next/server' export async function GET(req: NextRequest) { const q = req.nextUrl.searchParams.get('q') ?? '' const page = Number(req.nextUrl.searchParams.get('page') ?? '1') const results = await search(q, page) return NextResponse.json(results) } // GET /api/search?q=hello&page=2
Headers
import { headers } from 'next/headers' export async function GET() { const headerList = await headers() const auth = headerList.get('authorization') return NextResponse.json({ ok: true }, { headers: { 'X-Custom': 'value' }, }) }
Cookies
import { cookies } from 'next/headers' export async function GET() { const jar = await cookies() const token = jar.get('token')?.value return NextResponse.json({ token }) } export async function POST() { const jar = await cookies() jar.set('token', 'abc123', { httpOnly: true }) jar.delete('old-cookie') return NextResponse.json({ ok: true }) }
Form Data
export async function POST(req: Request) { const formData = await req.formData() const name = formData.get('name') as string const email = formData.get('email') as string await saveContact({ name, email }) return NextResponse.json({ ok: true }) }
Status Codes
export async function GET() { // not found return NextResponse.json( { error: 'Not found' }, { status: 404 } ) } export async function DELETE() { await db.item.delete() return new Response(null, { status: 204 }) // no content }

Intermediate Patterns

Error Handling
export async function GET(req: NextRequest, ctx: Ctx) { try { const { id } = await ctx.params const user = await db.user.findUniqueOrThrow({ where: { id }, }) return NextResponse.json(user) } catch (e: any) { if (e?.code === 'P2025') { return NextResponse.json( { error: 'Not found' }, { status: 404 } ) } return NextResponse.json( { error: 'Server error' }, { status: 500 } ) } }
Zod Validation
import { z } from 'zod' const CreateUser = z.object({ name: z.string().min(1), email: z.string().email(), }) export async function POST(req: Request) { const result = CreateUser.safeParse(await req.json()) if (!result.success) { return NextResponse.json( { errors: result.error.flatten().fieldErrors }, { status: 400 } ) } const user = await db.user.create({ data: result.data }) return NextResponse.json(user, { status: 201 }) }
CORS
const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', } export async function OPTIONS() { return new Response(null, { headers: corsHeaders }) } export async function GET() { return NextResponse.json({ ok: true }, { headers: corsHeaders, }) }
Auth Guard
import { auth } from '@/lib/auth' export async function GET() { const session = await auth() if (!session) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ) } const data = await getProtectedData(session.user.id) return NextResponse.json(data) }
Catch-All Route
// app/api/[...path]/route.ts interface Ctx { params: Promise<{ path: string[] }> } export async function GET(req: Request, ctx: Ctx) { const { path } = await ctx.params // GET /api/a/b/c → path = ['a', 'b', 'c'] return NextResponse.json({ path }) }
Streaming
export async function GET() { const encoder = new TextEncoder() const stream = new ReadableStream({ async start(controller) { for (const chunk of ['Hello', ' ', 'World']) { controller.enqueue(encoder.encode(chunk)) await new Promise((r) => setTimeout(r, 500)) } controller.close() }, }) return new Response(stream, { headers: { 'Content-Type': 'text/plain' }, }) }

Advanced Patterns

File Upload
export async function POST(req: Request) { const formData = await req.formData() const file = formData.get('file') as File const bytes = await file.arrayBuffer() const buffer = Buffer.from(bytes) await writeFile(`./uploads/${file.name}`, buffer) return NextResponse.json({ name: file.name, size: file.size, }) }
Server-Sent Events
export async function GET() { const encoder = new TextEncoder() const stream = new ReadableStream({ start(controller) { let count = 0 const interval = setInterval(() => { const data = `data: ${JSON.stringify({ count: count++ })}\n\n` controller.enqueue(encoder.encode(data)) if (count > 10) { clearInterval(interval) controller.close() } }, 1000) }, }) return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }, }) }
Webhook Handler
import crypto from 'crypto' export async function POST(req: Request) { const body = await req.text() const sig = req.headers.get('x-webhook-signature')! const expected = crypto .createHmac('sha256', process.env.WEBHOOK_SECRET!) .update(body) .digest('hex') if (sig !== expected) { return NextResponse.json( { error: 'Invalid signature' }, { status: 401 } ) } const event = JSON.parse(body) await processEvent(event) return NextResponse.json({ received: true }) }
Pagination
export async function GET(req: NextRequest) { const { searchParams } = req.nextUrl const page = Number(searchParams.get('page') ?? '1') const limit = Number(searchParams.get('limit') ?? '20') const skip = (page - 1) * limit const [items, total] = await Promise.all([ db.item.findMany({ skip, take: limit }), db.item.count(), ]) return NextResponse.json({ items, page, totalPages: Math.ceil(total / limit), }) }
Route Segment Config
// app/api/feed/route.ts // opt out of caching export const dynamic = 'force-dynamic' // revalidate every 60 seconds export const revalidate = 60 // set max duration (seconds) export const maxDuration = 30 // choose runtime export const runtime = 'edge' // or 'nodejs' export async function GET() { return NextResponse.json(await getFeed()) }
Proxy / Rewrite
// app/api/external/[...path]/route.ts interface Ctx { params: Promise<{ path: string[] }> } export async function GET(req: NextRequest, ctx: Ctx) { const { path } = await ctx.params const url = `https://api.example.com/${path.join('/')}` const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.API_KEY}` }, }) const data = await res.json() return NextResponse.json(data) } // keeps API key server-side
Last updated on