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.geo were removed in Next 15. For IP/geo on Vercel use ipAddress(request) / geolocation(request) from @vercel/functions; self-hosted, read x-forwarded-for or 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() or request.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

ConditionDefault cache
GET with no dynamic dataCached (static)
GET using request objectDynamic (no cache)
GET using cookies() / headers()Dynamic
POST, PUT, PATCH, DELETENever 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

Reading Headers and Cookies (from next/headers)

import { headers, cookies } from 'next/headers'

export async function GET() {
  const headersList = await headers()
  const token = headersList.get('authorization')

  const cookieStore = await cookies()
  const session = cookieStore.get('session')?.value

  return NextResponse.json({ token, session })
}

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[]