Vercel Logo

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

  1. Create apps/web/src/proxy.ts exporting a proxy function that receives a NextRequest
  2. Return NextResponse.next() with custom headers to continue to the route
  3. Add a config export with matcher to scope which paths run the proxy
Next.js 16 Breaking Change

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:

  1. Create a proxy file that intercepts all page requests
  2. Add X-Frame-Options and X-Content-Type-Options security headers
  3. Log the request method and URL to the console
  4. Configure the matcher to exclude static files and images

Implementation hints:

  • proxy.ts must be in src/ at the same level as app/, not inside the app directory
  • Export a function named proxy (not middleware)
  • Use NextResponse.next() to continue to the route with modified headers
  • The request parameter provides method, url, headers, cookies
  • Proxy runs after next.config.js redirects but before page rendering
  • Proxy uses Node.js runtime by default and cannot be changed to Edge in this file
  • Use the config export with matcher to control which paths run the proxy
  • Exclude _next/static, _next/image, and favicon.ico from the matcher

Step 1: Create the Proxy File

Create the proxy file at the correct location:

touch apps/web/src/proxy.ts

Add the proxy function with security headers:

apps/web/src/proxy.ts
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, not middleware
  • NextResponse.next() continues to the route, allowing you to modify response headers
  • The matcher regex 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):

apps/web/src/proxy.ts
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/debug
apps/web/src/app/api/debug/route.ts
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(),
  })
}

Try It

  1. Start the dev server:

    pnpm dev
  2. Check proxy execution in terminal: Navigate to http://localhost:3000 and watch your terminal. You should see:

    [Proxy] GET /
    
  3. 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>
    
  4. 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"
    }
  5. 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 push

Done-When

  • proxy.ts exists at apps/web/src/proxy.ts (same level as app/)
  • Browser DevTools shows X-Frame-Options: DENY header on page responses
  • Browser DevTools shows X-Content-Type-Options: nosniff header on page responses
  • Terminal shows [Proxy] GET / when visiting the homepage
  • http://localhost:3000/api/debug returns JSON with a requestId value
  • 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) { ... }
Still Stuck?

Ask your coding agent for help. Paste the error message and it can diagnose the issue.

Prompt: Debug Proxy Not Running
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

apps/web/src/proxy.ts
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

apps/web/src/app/api/debug/route.ts
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

  1. File location matters - proxy.ts must be at the same level as app/, typically src/proxy.ts or just proxy.ts at project root.

  2. Function name is proxy - This changed from middleware in Next.js 16. The old name still works but is deprecated.

  3. 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.

  4. NextResponse.next() continues routing - Call this to pass the request through with optional modifications. Without it, the request hangs.

  5. 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.

  6. Request headers vs response headers - Use NextResponse.next({ request: { headers } }) to modify headers seen by your routes. Set response.headers for headers sent to the client.

Learn More

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.