← Back to blog

Next.js Proxy for Developers: Migration and Recipes

August 20, 2026
Next.js Proxy for Developers: Migration and Recipes

Next.js Proxy is the file convention that replaces Middleware starting in Next.js 16, and if you're still running middleware.ts, the fix is straightforward: rename the file to proxy.ts, rename the exported function to proxy, and scope it with a matcher that excludes _next/static, _next/image, and favicon.ico. That last part matters more than most developers assume. Skip setting a matcher and your proxy function runs on every static asset request, which adds unnecessary latency.

Use next.config.js redirects for anything static, like sending /old-path to /new-path. Reach for Proxy when the decision depends on request-level data: a cookie, a header, a database lookup for tenant resolution. Proxy runs on the Node.js runtime by default in Next.js 16, so database clients work fine here, provided you keep the calls light.

Here's the minimal shape:

  • File: proxy.ts at the project root or inside src/
  • Export: export function proxy(request) { ... }
  • Optional: export const config = { matcher: [...] }

Pro Tip: If you're mid migration, run npx @next/codemod middleware-to-proxy before touching anything by hand. It handles the rename and the export change in one pass, and it's faster than manually hunting for every middleware.ts reference in a large monorepo.

Key Takeaways

Next.js Proxy replaces Middleware in Next.js 16, runs on Node.js by default, and belongs in your pipeline only for request-level decisions that a static next.config.js redirect cannot handle.

PointDetails
File conventionPlace proxy.ts at the project root or inside src/, export a proxy function, and add a scoped matcher.
Runtime defaultProxy runs on Node.js by default in Next.js 16, enabling database clients but requiring lightweight calls.
Execution orderHeaders, then next.config.js redirects, then Proxy, then rewrites, then filesystem and dynamic routes.
Migration pathRename middleware.ts to proxy.ts, rename the export, and run npx @next/codemod middleware-to-proxy.
Get it done fastHanadkubat offers fixed-price Proxy migration and tenant routing engagements with EU compliance notes included.

Table of Contents

What Are the File Conventions for Next.js Server Proxy?

A Proxy file lives in exactly one place: proxy.ts or proxy.js at your project root, or at the same directory level as app/ or pages/ if you're using a src/ layout. You get one proxy file per project, full stop. If your logic is getting complicated, split it into helper modules and import them into proxy.ts rather than trying to create a second entry point. Next.js won't recognize it anyway.

The file needs a proxy function as its export, either named or default depending on your style, plus an optional config object carrying a matcher array. This is the entire API surface. No lifecycle hooks, no separate config file, no build step beyond what Next.js already does.

Migrating from an older project is mechanical:

  • Rename middleware.ts to proxy.ts
  • Rename the exported middleware function to proxy
  • Run npx @next/codemod middleware-to-proxy to catch edge cases you'd miss by hand
  • Confirm your matcher still excludes static asset paths after the rename

Pro Tip: Don't skip the explicit matcher just because your proxy logic seems harmless. Even a no-op proxy function adds a network hop for every request that matches, including ones you never intended to touch.

How Do You Write the Proxy Function and Matcher Config?

The function signature is simple: proxy(request), with an optional second event parameter for background work. The request object is a NextRequest, and it exposes everything you'd expect: method, url, headers, and cookies. You read these to make routing decisions, then return a response.

Developer workspace with dark screen and tech desk items

The matcher is where most bugs live. It accepts path patterns, and the standard exclusion pattern uses a negative lookahead to skip static files:

matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
  • Prefer narrow matchers over one that runs on every route
  • Test matcher regex locally before deploying; a mismatched pattern either runs too broadly or misses the paths you actually care about
  • Remember public/ assets need the same exclusion treatment as _next/static

One config detail trips people up: proxy files don't accept a runtime option anymore. They default to Node.js, and that's fixed.

Pro Tip: Log the matched path with a temporary console.log(request.nextUrl.pathname) during development. It's the fastest way to confirm your matcher is actually excluding what you think it's excluding.

When Should You Use next(), rewrite(), or redirect()?

NextResponse.next() continues the request down the normal routing pipeline. Use it when you're just attaching headers, either to the request (so route handlers can read them) or to the response (so the client sees them). This is the most common call inside any proxy function, by a wide margin.

NextResponse.rewrite() changes the resolved destination without changing the URL the browser sees. Clone the request's nextUrl, modify the pathname or hostname, and pass it to rewrite(). This works for internal routing (send /blog to /content/blog) and for external proxying, where you forward to a different host entirely.

NextResponse.redirect() is different: it sends the browser a 3xx response and the URL bar changes. Use this for genuine navigation changes, not internal routing decisions.

  • Set request headers so downstream route handlers can read tenant IDs, request IDs, or auth context
  • Set response headers for anything the client needs, like security headers or CORS values
  • Watch header size. Large forwarded headers can trigger a 431 response if you're not selective about what you pass through

The NextRequest and NextResponse APIs cover cookie manipulation too, which matters if your proxy handles session tokens.

Where Does Proxy Run in the Request Pipeline?

The order matters: headers get applied first, then next.config.js redirects run, then your proxy.ts executes, and only after that do rewrites (beforeFiles, afterFiles, fallback) get evaluated, followed by filesystem routes, dynamic routes, and finally the 404 fallback.

That ordering has a practical consequence. If a static redirect can happen in next.config.js, do it there. It runs before Proxy and doesn't cost you a function invocation. Save Proxy for logic that genuinely needs request-level data, like a cookie value or a header that isn't known until the request arrives.

  • Proxy defaults to the Node.js runtime in Next.js 16, which is why database clients work inside it
  • The runtime config option is no longer accepted in proxy files
  • Node.js runtime access is convenient for tenant lookups, but every database call adds latency directly to the request path, so cache aggressively or keep the query trivial

Moving to Node.js as the default runtime isn't cosmetic. It's what makes server-side integrations like tenant database lookups practical inside the pipeline at all, as long as you keep those calls fast.

What Do Common Next.js Proxy Recipes Look Like?

Three patterns cover most real-world proxy work: security headers, request tracing, and tenant resolution.

Security headers, the simplest case:

export function proxy(request) {
  const response = NextResponse.next();
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  return response;
}

Request ID propagation for tracing across services, a pattern Vercel's own examples demonstrate for logging and observability:

const requestId = crypto.randomUUID();
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-request-id', requestId);
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set('x-request-id', requestId);

Tenant routing by subdomain, a documented Vercel multi-tenant pattern:

const hostname = request.headers.get('host');
const subdomain = hostname.split('.')[0];
const url = request.nextUrl.clone();
url.pathname = `/tenants/${subdomain}${url.pathname}`;
return NextResponse.rewrite(url);

External rewrite, common for reverse-proxying to a separate service, mirrors community reverse-proxy setups that adjust the host header on the way out.

Whatever pattern you use, strip inbound x-tenant-* headers before setting your own. Never trust a client-supplied tenant identifier; Vercel's documentation flags this explicitly as a security requirement, not a suggestion.

Pro Tip: Generate the request ID once and reuse the variable for both the request header and response header. Calling crypto.randomUUID() twice gives you two different IDs, which defeats the entire point of tracing a single request.

How Do You Migrate From middleware.ts Without Breaking Things?

Run through this checklist in order: rename the file, rename the export, run the codemod, re-check your matcher, move anything runtime-specific, then run integration tests against the paths your proxy touches.

The pitfalls are predictable. Forgetting the matcher means Proxy fires on static assets. Leaving old inbound tenant headers unstripped opens a spoofing risk. Heavy database calls inside Proxy stack directly onto your response time since blocking lookups delay rendering for every request that hits them.

  • Cache tenant lookups instead of querying on every request
  • Offload non-critical work with event.waitUntil() so it runs after the response is sent, a pattern borrowed from the FetchEvent API
  • Avoid blocking I/O on any path you expect to be hot

For testing, write unit tests against your matcher logic directly, integration tests that hit real proxy paths and assert on headers, and keep a local smoke test script you run before every deploy.

Pro Tip: Treat your matcher regex as its own testable unit. A one-line change to an exclusion pattern has broken production traffic for teams who assumed "it looked right" was good enough.

Should You Use Proxy or next.config.js Redirects?

If the decision depends on a cookie, a header, or a database lookup, that's Proxy territory. If it's a static path mapping or a blanket host-wide redirect, next.config.js handles it earlier in the pipeline and is simpler to cache.

For teams handling EU user data, route sensitive requests to EU-resident backends and set validated tenant headers server-side rather than trusting anything from the client. This matters under GDPR and, depending on your AI features, the EU AI Act's data handling expectations.

  • Static, predictable mapping → next.config.js redirects or rewrites
  • Request-level decision (auth, tenant, A/B bucket) → Proxy
  • Sensitive or regulated data → confirm the backend target is EU-resident, not just the frontend

Pro Tip: Default to next.config.js whenever you can. It runs before Proxy in the pipeline and adds nothing to your function invocation count, which matters at scale.

Why Isn't My Proxy Header or Rewrite Working?

Start with the basics: confirm the file is actually named proxy.ts and sits at the right level, confirm the export is named proxy, and check the matcher isn't silently excluding the path you're testing.

  • Add a temporary console.log(request.nextUrl.pathname) and confirm it fires for the request you're debugging
  • Missing headers on the client almost always mean either the matcher excluded the request, or NextResponse.next() wasn't returned with the modified headers attached
  • Restart the dev server after any proxy file change; hot reload doesn't always pick up matcher changes cleanly
  • Run a direct curl against the path in question and inspect the response headers
  • On your hosting platform, confirm rewrites and proxying are actually supported, and check whether trailing-slash redirects interfere with your matcher patterns

A pragmatic take on when Proxy earns its place

I reach for Proxy for tenant routing, lightweight auth checks, and header injection for tracing. Nothing heavier. If a task needs real business logic or session state, it belongs in a route handler or a background job, not in the request path every visitor hits.

Get Proxy Migration Done in Weeks, Not Months

Most teams lose a week to Proxy migration not because the codemod fails, but because the matcher edge cases and tenant header security get discovered in production. Hanadkubat runs fixed-price migration and implementation engagements for B2B SaaS teams that need this done correctly the first time, shipped in a defined sprint rather than an open-ended retainer.

Hanadkubat

What you get: a full migration checklist run against your codebase, the codemod executed and validated against your existing routes, unit and integration tests covering matcher logic and header propagation, and EU compliance notes on tenant header handling and data residency where GDPR or EU AI Act scope applies. This is the same kind of infrastructure work behind Hanad's background at BMW, Deutsche Bahn, and Bundesrechenzentrum Austria, applied to a smaller, fixed-price engagement.

If your team is stuck mid-migration or scoping a larger multi-tenant rebuild, check the services overview for current engagement structures and start a conversation about scope.

Sources

FAQ

Is Next.js Still Relevant in 2026?

Yes. Next.js 16 introduced the Proxy convention specifically to clarify its request interception model, and active investment from Vercel in runtime defaults and tooling like the migration codemod signals continued core development.

Is Next.js SSR or CSR?

Next.js supports both server-side rendering and client-side rendering, along with static generation, and lets you choose per route depending on whether content needs to be dynamic or can be cached.

Is Next.js Obsolete?

No. The Middleware to Proxy rename in Next.js 16 reflects active framework evolution, not stagnation, and the change was made specifically to correct a conceptual confusion around the old Middleware naming.

How Do I Create a Proxy Configuration in Next.js?

Create a proxy.ts file at your project root (or inside src/), export a proxy function that receives the request, and add a config object with a matcher array that excludes _next/static, _next/image, and favicon.ico. For a migration from an existing Middleware setup, run npx @next/codemod middleware-to-proxy and verify your matcher and tenant header handling afterward, or have a fixed-price engagement handle the migration end to end.