Next.js Cheatsheet
Route Handlers (API)
Use this Next.js reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Route Handler Basics
Create a route.ts (or route.js) file inside app/. No page.tsx can exist in the same folder.
app/
└── api/
└── hello/
└── route.ts → GET /api/hello// app/api/hello/route.ts import { NextResponse } from 'next/server' export async function GET() { return NextResponse.json({ message: 'Hello' }) }
Supported HTTP Methods
Export a named function for each HTTP verb:
export async function GET(request: NextRequest) {} export async function POST(request: NextRequest) {} export async function PUT(request: NextRequest) {} export async function PATCH(request: NextRequest) {} export async function DELETE(request: NextRequest) {} export async function HEAD(request: NextRequest) {} export async function OPTIONS(request: NextRequest) {}
Unhandled methods return 405 Method Not Allowed automatically.
NextRequest and NextResponse
import { NextRequest, NextResponse } from 'next/server' export async function GET(request: NextRequest) { // URL and params const url = request.nextUrl const q = url.searchParams.get('q') // Headers const auth = request.headers.get('authorization') // Cookies const sessionCookie = request.cookies.get('session')?.value return NextResponse.json({ q, auth }) }
request.ip/request.geowere removed in Next 15. For IP/geo on Vercel useipAddress(request)/geolocation(request)from@vercel/functions; self-hosted, readx-forwarded-foror your proxy's headers.
Reading Request Body
export async function POST(request: NextRequest) { // JSON body const body = await request.json() // Form data const formData = await request.formData() const name = formData.get('name') as string // Raw text const text = await request.text() // Raw ArrayBuffer (e.g., webhooks needing raw bytes) const buffer = await request.arrayBuffer() return NextResponse.json({ received: true }) }
For Stripe webhooks needing the raw body, use
request.arrayBuffer()orrequest.text()before any parsing.
NextResponse Methods
// JSON response NextResponse.json({ ok: true }) NextResponse.json({ error: 'Not found' }, { status: 404 }) // Plain text / HTML new Response('Hello', { headers: { 'Content-Type': 'text/plain' } }) // Redirect NextResponse.redirect(new URL('/login', request.url)) NextResponse.redirect('https://example.com', { status: 301 }) // Rewrite (URL stays the same) NextResponse.rewrite(new URL('/api/v2/resource', request.url)) // Set cookies on response const res = NextResponse.json({ ok: true }) res.cookies.set('session', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 24 * 7, // 7 days path: '/', }) return res // Delete a cookie res.cookies.delete('session')
Dynamic Route Handlers
// app/api/users/[id]/route.ts export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params const user = await db.users.findUnique({ where: { id } }) if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 }) return NextResponse.json(user) }
CRUD Example
// app/api/posts/route.ts import { NextRequest, NextResponse } from 'next/server' export async function GET(request: NextRequest) { const { searchParams } = request.nextUrl const page = Number(searchParams.get('page') ?? '1') const posts = await db.posts.findMany({ skip: (page - 1) * 10, take: 10 }) return NextResponse.json(posts) } export async function POST(request: NextRequest) { const body = await request.json() const post = await db.posts.create({ data: body }) return NextResponse.json(post, { status: 201 }) }
// app/api/posts/[id]/route.ts export async function PUT( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params const body = await request.json() const post = await db.posts.update({ where: { id }, data: body }) return NextResponse.json(post) } export async function DELETE( _request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params await db.posts.delete({ where: { id } }) return new Response(null, { status: 204 }) }
Caching Behavior
| Condition | Default cache |
|---|---|
GET with no dynamic data | Cached (static) |
GET using request object | Dynamic (no cache) |
GET using cookies() / headers() | Dynamic |
POST, PUT, PATCH, DELETE | Never cached |
Force static:
export const dynamic = 'force-static' export async function GET() { /* ... */ }
Force dynamic:
export const dynamic = 'force-dynamic'
Revalidate interval:
export const revalidate = 60 // seconds
Error Handling
export async function GET() { try { const data = await riskyOperation() return NextResponse.json(data) } catch (err) { console.error(err) return NextResponse.json( { error: 'Internal Server Error' }, { status: 500 } ) } }
CORS Headers
const CORS_HEADERS = { '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, { status: 204, headers: CORS_HEADERS }) } export async function GET() { return NextResponse.json({ data: 'ok' }, { headers: CORS_HEADERS }) }
Streaming Responses
import { OpenAI } from 'openai' export async function POST(request: NextRequest) { const { prompt } = await request.json() const openai = new OpenAI() const stream = await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }], stream: true, }) const encoder = new TextEncoder() const readable = new ReadableStream({ async start(controller) { for await (const chunk of stream) { const text = chunk.choices[0]?.delta?.content ?? '' controller.enqueue(encoder.encode(text)) } controller.close() }, }) return new Response(readable, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }) }
Edge Runtime
export const runtime = 'edge' // faster cold starts, limited Node APIs export async function GET() { return new Response('Edge response') }
Edge runtime lacks: Node.js built-ins (
fs,path, native crypto), most npm packages that use them.
Segment Config Options
export const dynamic = 'auto' | 'force-dynamic' | 'error' | 'force-static' export const revalidate = false | 0 | number // seconds export const runtime = 'nodejs' | 'edge' export const maxDuration = 30 // seconds (Vercel Pro+) export const preferredRegion = 'auto' | 'global' | 'home' | string[]