Next.js Cheatsheet

Proxy & Middleware

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.

What Is Proxy (formerly Middleware)?

Request-interception code runs before every matched request — before the page renders, before Route Handlers, before cached content is served.

  • Next 16: the file is proxy.ts exporting a proxy function, and it runs on the Node.js runtime by default.
  • Next ≤15 (legacy): the file is middleware.ts exporting a middleware function. It defaults to the Edge Runtime; the Node.js runtime is a stable opt-in since 15.5 (export const config = { runtime: 'nodejs' }).

Everything below (NextRequest/NextResponse, matchers, cookies, headers) is identical in both — only the file/function name and default runtime differ.

Place the file at the project root (next to app/) or inside src/ if using the src/ layout.

├── app/
├── proxy.ts          ← root level (middleware.ts in Next ≤15)
└── package.json

Basic Shape

// proxy.ts  (middleware.ts + `export function middleware` in Next ≤15)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  // Inspect request, then return a NextResponse
  return NextResponse.next()   // continue as normal
}

// Which paths to run on (optional — defaults to all routes)
export const config = {
  matcher: '/dashboard/:path*',
}

NextRequest in Proxy

import { NextRequest } from 'next/server'
import { ipAddress, geolocation } from '@vercel/functions'

export function proxy(request: NextRequest) {
  const { pathname, search, origin } = request.nextUrl
  const method = request.method
  const ip = ipAddress(request)                 // client IP (Vercel)
  const { country, city } = geolocation(request) // geo data (Vercel)
  const token = request.cookies.get('session')?.value
  const auth = request.headers.get('authorization')
}

request.ip and request.geo were removed in Next 15 — use ipAddress() / geolocation() from @vercel/functions (Vercel), or read x-forwarded-for / your platform's headers when self-hosting.

Returning Responses

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  // 1. Continue (pass through)
  return NextResponse.next()

  // 2. Redirect
  return NextResponse.redirect(new URL('/login', request.url))

  // 3. Rewrite (URL in browser stays the same)
  return NextResponse.rewrite(new URL('/api/v2/resource', request.url))

  // 4. Return a response directly (short-circuit)
  return new NextResponse('Unauthorized', { status: 401 })

  // 5. Return JSON
  return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}

Reading and Setting Cookies

export function proxy(request: NextRequest) {
  // Read cookies
  const session = request.cookies.get('session')?.value
  const allCookies = request.cookies.getAll()

  // Set / modify cookies on the response
  const response = NextResponse.next()
  response.cookies.set('visited', 'true', {
    httpOnly: true,
    maxAge: 60 * 60 * 24,
  })
  response.cookies.delete('old-cookie')

  return response
}

Reading and Setting Headers

export function proxy(request: NextRequest) {
  // Forward a custom header to the page/layout
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-pathname', request.nextUrl.pathname)

  const response = NextResponse.next({
    request: { headers: requestHeaders },
  })

  // Set response headers
  response.headers.set('x-frame-options', 'DENY')
  response.headers.set('x-content-type-options', 'nosniff')

  return response
}

Read the forwarded header in a Server Component:

import { headers } from 'next/headers'

export default async function Page() {
  const headersList = await headers()
  const pathname = headersList.get('x-pathname')
  return <div>Current path: {pathname}</div>
}

matcher — Controlling Which Routes Run

export const config = {
  matcher: '/dashboard/:path*',
}

Multiple patterns:

export const config = {
  matcher: [
    '/dashboard/:path*',
    '/admin/:path*',
    '/api/protected/:path*',
  ],
}

Exclude paths with negative lookahead:

export const config = {
  matcher: [
    /*
     * Match all except:
     * - _next/static  (static files)
     * - _next/image   (image optimization)
     * - favicon.ico
     * - public folder files
     */
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

Match by condition inside the function (no config matcher needed):

export function proxy(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/api')) {
    return NextResponse.next()
  }
  // ...API-only logic
}

Auth Guard Pattern

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const PUBLIC_ROUTES = ['/', '/login', '/signup', '/about']

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl
  const session = request.cookies.get('session')?.value

  const isPublic = PUBLIC_ROUTES.includes(pathname)

  if (!session && !isPublic) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(loginUrl)
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

JWT Verification in Proxy

import { jwtVerify } from 'jose'       // jose works on Edge; jsonwebtoken does NOT

const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!)

export async function proxy(request: NextRequest) {
  const token = request.cookies.get('session')?.value

  if (!token) return NextResponse.redirect(new URL('/login', request.url))

  try {
    await jwtVerify(token, SECRET)
    return NextResponse.next()
  } catch {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

jose (Web Crypto API) works on every runtime. jsonwebtoken is Node.js-only — fine in Next 16 proxy (Node runtime), broken on Edge-runtime middleware.

Internationalization (i18n) Routing

import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const locales = ['en', 'fr', 'de']
const defaultLocale = 'en'

function getLocale(request: NextRequest) {
  const headers = { 'accept-language': request.headers.get('accept-language') ?? '' }
  const languages = new Negotiator({ headers }).languages()
  return match(languages, locales, defaultLocale)
}

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl
  const hasLocale = locales.some(l => pathname.startsWith(`/${l}/`) || pathname === `/${l}`)

  if (!hasLocale) {
    const locale = getLocale(request)
    return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url))
  }

  return NextResponse.next()
}

A/B Testing / Feature Flags

export function proxy(request: NextRequest) {
  const bucket = request.cookies.get('ab-bucket')?.value ?? (Math.random() < 0.5 ? 'a' : 'b')

  const response = bucket === 'b'
    ? NextResponse.rewrite(new URL('/b-variant' + request.nextUrl.pathname, request.url))
    : NextResponse.next()

  response.cookies.set('ab-bucket', bucket, { maxAge: 60 * 60 * 24 * 30 })
  return response
}

Security Headers via Proxy

export function proxy(request: NextRequest) {
  const response = NextResponse.next()

  response.headers.set('X-Frame-Options', 'DENY')
  response.headers.set('X-Content-Type-Options', 'nosniff')
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline'"
  )

  return response
}

Edge Runtime Constraints (only when running on Edge)

These limits apply only to code on the Edge Runtime — Next ≤15 middleware.ts by default, or Route Handlers with runtime = 'edge'. Next 16 proxy.ts runs on Node.js and has full Node API access.

AvailableNot Available
Web Fetch APIfs, path, crypto (Node)
jose, zod, edge-compatible packagesjsonwebtoken, bcrypt
TextEncoder / TextDecoderMost Node.js built-ins
URL, URLSearchParamsNative addons / .node files
Request, Response, HeadersLong-running CPU tasks

Execution Order

  1. next.config.ts headers()
  2. next.config.ts redirects()
  3. Proxy / Middleware
  4. next.config.ts beforeFiles rewrites
  5. Filesystem routes (public/, _next/static, pages)
  6. next.config.ts afterFiles rewrites
  7. Dynamic routes
  8. next.config.ts fallback rewrites