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.tsexporting aproxyfunction, and it runs on the Node.js runtime by default. - Next ≤15 (legacy): the file is
middleware.tsexporting amiddlewarefunction. 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.ipandrequest.geowere removed in Next 15 — useipAddress()/geolocation()from@vercel/functions(Vercel), or readx-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 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.jsonwebtokenis 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.
| Available | Not Available |
|---|---|
| Web Fetch API | fs, path, crypto (Node) |
jose, zod, edge-compatible packages | jsonwebtoken, bcrypt |
TextEncoder / TextDecoder | Most Node.js built-ins |
URL, URLSearchParams | Native addons / .node files |
Request, Response, Headers | Long-running CPU tasks |
Execution Order
next.config.tsheaders()next.config.tsredirects()- Proxy / Middleware
next.config.tsbeforeFilesrewrites- Filesystem routes (
public/,_next/static, pages) next.config.tsafterFilesrewrites- Dynamic routes
next.config.tsfallbackrewrites