Proxy Basics
A user hits /dashboard without logging in. Your page component checks auth, realizes they're not authenticated, and redirects. But the damage is done: the page already started rendering, the database query already ran, and you've wasted server resources on an unauthorized request.
You need to intercept requests before they reach your routes. Check auth before rendering. Add security headers to every response. Log requests for debugging. Next.js 16 introduces proxy.ts for exactly this: one file, central control over your request pipeline.
Outcome
A working proxy.ts that adds security headers to all responses, logs request information to the console, and demonstrates the request interception lifecycle.
Fast Track
- Create
apps/web/src/proxy.tsexporting aproxyfunction that receives aNextRequest - Return
NextResponse.next()with custom headers to continue to the route - Add a
configexport withmatcherto scope which paths run the proxy
In Next.js 16, proxy.ts replaces the old middleware.ts convention. If you're upgrading from an earlier version, rename your file from middleware.ts to proxy.ts and change the exported function name from middleware to proxy. A codemod (an automated code transformation tool that updates your codebase to new patterns) is available: npx @next/codemod@canary upgrade.
Building on Errors and Not Found
In Errors and Not Found, you added error boundaries that catch runtime errors and 404 pages for missing routes. Those handle problems after routing. Proxy handles logic before routing: authentication checks, header injection, request logging. Together they cover the full request lifecycle: intercept with proxy, render with routes, recover with error boundaries.
Self-Paced Exercise
Requirements:
- Create a proxy file that intercepts all page requests
- Add
X-Frame-OptionsandX-Content-Type-Optionssecurity headers - Log the request method and URL to the console
- Configure the matcher to exclude static files and images
Implementation hints:
proxy.tsmust be insrc/at the same level asapp/, not inside the app directory- Export a function named
proxy(notmiddleware) - Use
NextResponse.next()to continue to the route with modified headers - The
requestparameter providesmethod,url,headers,cookies - Proxy runs after
next.config.jsredirects but before page rendering - Proxy uses Node.js runtime by default and cannot be changed to Edge in this file
- Use the
configexport withmatcherto control which paths run the proxy - Exclude
_next/static,_next/image, andfavicon.icofrom the matcher
Step 1: Create the Proxy File
Create the proxy file at the correct location:
touch apps/web/src/proxy.tsAdd the proxy function with security headers:
import { type NextRequest, NextResponse } from 'next/server'
export function proxy(request: NextRequest) {
// biome-ignore lint/suspicious/noConsole: Intentional for request logging demonstration
console.log(`[Proxy] ${request.method} ${request.nextUrl.pathname}`)
// Continue to the route with added security headers
const response = NextResponse.next()
// Security headers
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
return response
}
// Configure which paths run the proxy
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!_next/static|_next/image|favicon.ico).*)',
],
}Key points:
- The function is named
proxy, notmiddleware NextResponse.next()continues to the route, allowing you to modify response headers- The
matcherregex excludes Next.js internal paths to avoid running on every static asset - Console logging happens on the server, visible in your terminal running
pnpm dev
Step 2: Understand the Request Lifecycle
┌─────────────────────────────────────────────────────────────────────┐
│ Next.js Request Lifecycle │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. DNS + TLS │
│ │ │
│ 2. next.config.js redirects/rewrites │
│ │ │
│ 3. proxy.ts ← YOU ARE HERE │
│ │ • Check authentication │
│ │ • Add/modify headers │
│ │ • Log requests │
│ │ • Redirect users │
│ │ │
│ 4. Route matching (app/page.tsx, app/api/route.ts) │
│ │ │
│ 5. Page/Route rendering │
│ │ │
│ 6. Response sent to client │
│ │
└─────────────────────────────────────────────────────────────────────┘
Proxy runs on every matching request, including during prefetches. Keep it fast: no database queries, no heavy computation. Read cookies and headers, make routing decisions, continue quickly.
Step 3: Add Request Headers for Downstream Routes
Proxy can also add headers that your routes can read. This is useful for passing context like user IDs or correlation IDs (unique identifiers that track a single request across multiple services for debugging):
import { type NextRequest, NextResponse } from 'next/server'
export function proxy(request: NextRequest) {
// biome-ignore lint/suspicious/noConsole: Intentional for request logging demonstration
console.log(`[Proxy] ${request.method} ${request.nextUrl.pathname}`)
// Create a request headers object with additional headers
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-request-id', crypto.randomUUID())
requestHeaders.set('x-pathname', request.nextUrl.pathname)
// Continue to route with modified request headers
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
// Security headers on the response
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('X-Request-Id', requestHeaders.get('x-request-id') || '')
return response
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}Now your route handlers and pages can read x-request-id from the incoming headers for logging or debugging.
Step 4: Reading Headers in a Route
Create a simple API route to verify the proxy headers are being passed:
mkdir -p apps/web/src/app/api/debugimport { headers } from 'next/headers'
export async function GET() {
const headersList = await headers()
return Response.json({
requestId: headersList.get('x-request-id'),
pathname: headersList.get('x-pathname'),
timestamp: new Date().toISOString(),
})
}Try It
-
Start the dev server:
pnpm dev -
Check proxy execution in terminal: Navigate to
http://localhost:3000and watch your terminal. You should see:[Proxy] GET / -
Verify security headers in browser: Open DevTools → Network tab → click on the document request → Headers tab. Look for:
X-Frame-Options: DENY X-Content-Type-Options: nosniff X-Request-Id: <uuid> -
Test the debug endpoint: Navigate to
http://localhost:3000/api/debug. You should see JSON with the request ID:{ "requestId": "550e8400-e29b-41d4-a716-446655440000", "pathname": "/api/debug", "timestamp": "2026-01-08T12:00:00.000Z" } -
Verify static files are excluded: In the terminal, you should NOT see
[Proxy]logs for paths like/_next/static/*. The matcher excludes these.
Expected terminal output:
┌ @repo/web#dev ─────────────────────────────────────────────────┐
│ ▲ Next.js 16.1.1 (Turbopack) │
│ - Local: http://localhost:3000 │
│ ✓ Ready in 436ms │
└────────────────────────────────────────────────────────────────┘
[Proxy] GET /
[Proxy] GET /api/debug
Commit
git add -A
git commit -m "feat: add proxy with security headers and request logging"
git pushDone-When
proxy.tsexists atapps/web/src/proxy.ts(same level asapp/)- Browser DevTools shows
X-Frame-Options: DENYheader on page responses - Browser DevTools shows
X-Content-Type-Options: nosniffheader on page responses - Terminal shows
[Proxy] GET /when visiting the homepage http://localhost:3000/api/debugreturns JSON with arequestIdvalue- Static file requests (
_next/static/*) do NOT trigger proxy logs
Troubleshooting
Proxy not running at all
Check the file location. proxy.ts must be at src/proxy.ts, at the same level as the app/ directory. It cannot be inside app/:
apps/web/src/
├── proxy.ts ← Correct location
├── app/
│ └── page.tsx
Also verify the function is named proxy, not middleware:
// Correct
export function proxy(request: NextRequest) { ... }
// Wrong (old convention)
export function middleware(request: NextRequest) { ... }Headers not appearing on responses
Make sure you're returning the response from NextResponse.next():
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
return response // Don't forget to return!Also check the request isn't excluded by your matcher. The header won't appear on requests that don't match.
Proxy running on static files
Your matcher pattern isn't excluding static paths. Use this pattern:
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}The (?!...) is a negative lookahead that excludes paths starting with those prefixes.
Type errors with NextRequest
Import types from next/server:
import { type NextRequest, NextResponse } from 'next/server'If you're seeing type errors about the function signature, ensure you're exporting the function (not a default export):
// Correct
export function proxy(request: NextRequest) { ... }
// Wrong
export default function proxy(request: NextRequest) { ... }Ask your coding agent for help. Paste the error message and it can diagnose the issue.
My `proxy.ts` isn't executing in Next.js 16. I don't see my console.log statements.
**My proxy file:**
```tsx
// File location: _____
// Example: src/proxy.ts (must be at src root, not in app/)
___PASTE_YOUR_PROXY_TS___
```
**My next.config.ts (if using matcher):**
```tsx
___PASTE_RELEVANT_CONFIG___
```
**The route I'm testing:** /_____
**What I expect to happen:**
_____
**What actually happens:**
- [ ] No logs appear in terminal
- [ ] Page loads but proxy logic doesn't run
- [ ] Error message: _____
**Checklist:**
- [ ] proxy.ts is at `src/proxy.ts` (not `src/app/proxy.ts`)
- [ ] File exports a `proxy` function (not default export)
- [ ] Matcher pattern matches my test route
- [ ] Dev server was restarted after creating proxy.ts
Why isn't my proxy executing and how do I fix it?Solution
Complete implementation
Proxy File
import { type NextRequest, NextResponse } from 'next/server'
export function proxy(request: NextRequest) {
// biome-ignore lint/suspicious/noConsole: Intentional for request logging demonstration
console.log(`[Proxy] ${request.method} ${request.nextUrl.pathname}`)
// Create request headers with additional context
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-request-id', crypto.randomUUID())
requestHeaders.set('x-pathname', request.nextUrl.pathname)
// Continue to route with modified request headers
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
// Security headers on the response
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('X-Request-Id', requestHeaders.get('x-request-id') || '')
return response
}
// Only run proxy on page routes, not static assets
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!_next/static|_next/image|favicon.ico).*)',
],
}Debug API Route
import { headers } from 'next/headers'
export async function GET() {
const headersList = await headers()
return Response.json({
requestId: headersList.get('x-request-id'),
pathname: headersList.get('x-pathname'),
timestamp: new Date().toISOString(),
})
}Key Implementation Notes
-
File location matters -
proxy.tsmust be at the same level asapp/, typicallysrc/proxy.tsor justproxy.tsat project root. -
Function name is
proxy- This changed frommiddlewarein Next.js 16. The old name still works but is deprecated. -
Node.js runtime only - Unlike the old middleware.ts, you cannot configure Edge runtime in proxy files. This gives you full access to Node.js APIs.
-
NextResponse.next()continues routing - Call this to pass the request through with optional modifications. Without it, the request hangs. -
Matcher excludes static files - The regex
(?!_next/static|_next/image|favicon.ico)prevents proxy from running on every asset request, which would slow down your app. -
Request headers vs response headers - Use
NextResponse.next({ request: { headers } })to modify headers seen by your routes. Setresponse.headersfor headers sent to the client.
Learn More
- Proxy - Getting started guide
- proxy.ts - File convention reference
- NextRequest - Request object API
- NextResponse - Response object API
What's Next
You've built request interception that runs before every page render. Security headers protect against common attacks. Request logging helps with debugging. The proxy pattern scales to authentication, A/B testing, geolocation routing, and more.
In Section 2, you'll dive into core Next.js features: data fetching patterns, caching strategies, and API routes. The proxy you built here will work alongside those features, intercepting requests before they reach your data-fetching code.
Was this helpful?