Skip to content
Vibe Code Basics
Next.js

Next.js Proxy (Formerly Middleware): Authentication, Redirects & Headers in Next.js 16

In Next.js 16, middleware.ts was renamed to proxy.ts. Learn how proxy works, how to protect routes, handle redirects and rewrites, set headers, and why you must still check auth in your data layer.

By Vibe Code Basics Editorial TeamPublished Updated 3 min read
On this page (13)

In Next.js 16, "middleware" was renamed to "proxy". You now create a proxy.ts file in your project root (or src/) and export a function named proxy. It runs before a request is completed, so you can redirect, rewrite, set headers or cookies, and do quick authentication checks. Proxy runs on the Node.js runtime.

If your AI agent creates a middleware.ts file, it's working from older training data. That's a perfect example of why you should give agents version-matched docs and context.

New to Next.js? Start with Getting Started with Next.js.

What changed from middleware to proxy

Next.js 15 and earlierNext.js 16
File namemiddleware.tsproxy.ts
Export namemiddlewareproxy
RuntimeEdge (default)Node.js (not configurable)
Config flagsskipMiddlewareUrlNormalizeskipProxyUrlNormalize

The middleware convention still works but is deprecated; keep it only if you specifically need the Edge runtime. To migrate automatically, run the official codemod:

npx @next/codemod@canary upgrade latest

Or rename by hand: mv middleware.ts proxy.ts and rename the exported function to proxy.

Your first proxy

// proxy.ts
import { NextResponse, type NextRequest } from 'next/server';

export function proxy(request: NextRequest) {
  return NextResponse.next();
}

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

The matcher limits which paths run the proxy. Always use one, since proxy runs on every matching request, including prefetches.

Protecting routes (optimistic auth checks)

// proxy.ts
import { NextResponse, type NextRequest } from 'next/server';

const protectedPrefixes = ['/dashboard', '/settings'];
const authPages = ['/login', '/signup'];

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const hasSession = request.cookies.has('session');

  if (protectedPrefixes.some((p) => pathname.startsWith(p)) && !hasSession) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    return NextResponse.redirect(loginUrl);
  }

  if (authPages.includes(pathname) && hasSession) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*', '/login', '/signup'],
};

This gives users a fast redirect, but notice it only checks that a cookie exists. That's intentional: proxy should do cheap, optimistic checks.

Why proxy alone is not security

Never rely on proxy (or middleware) as your only authentication check. In March 2025, CVE-2025-29927 (opens in a new tab) showed that a crafted request header could skip middleware entirely in self-hosted Next.js apps. Apps that only checked auth in middleware were wide open.

The Next.js authentication guide (opens in a new tab) recommends a layered approach:

  1. Proxy: optimistic redirects based on the presence of a session cookie.
  2. Data access layer: verify the session and check permissions right where you read or write data, in Server Components, Server Actions and Route Handlers.
// lib/dal.ts
import 'server-only';
import { cache } from 'react';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export const verifySession = cache(async () => {
  const token = (await cookies()).get('session')?.value;
  const session = await decryptAndValidate(token); // your auth library
  if (!session) redirect('/login');
  return session;
});

Then call await verifySession() before returning any private data. This is one of the most important items on our vibe coding security checklist, and keep Next.js updated so you get security patches.

Redirects

For simple, permanent redirects, prefer redirects in next.config.ts, which is faster and doesn't run code:

// next.config.ts
const nextConfig = {
  async redirects() {
    return [{ source: '/old-blog/:slug', destination: '/blog/:slug', permanent: true }];
  },
};
export default nextConfig;

Use proxy for redirects that depend on the request, such as language or cookies:

export function proxy(request: NextRequest) {
  const lang = request.headers.get('accept-language') ?? '';
  if (request.nextUrl.pathname === '/' && lang.startsWith('es')) {
    return NextResponse.redirect(new URL('/es', request.url));
  }
  return NextResponse.next();
}

Rewrites

A rewrite serves a different route without changing the URL in the browser. This is handy for A/B tests:

export function proxy(request: NextRequest) {
  if (request.nextUrl.pathname !== '/') return NextResponse.next();

  const variant = request.cookies.get('ab-home')?.value ?? (Math.random() < 0.5 ? 'a' : 'b');
  const response =
    variant === 'b'
      ? NextResponse.rewrite(new URL('/home-b', request.url))
      : NextResponse.next();

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

Setting headers

export function proxy(request: NextRequest) {
  const response = NextResponse.next();
  response.headers.set('X-Request-ID', crypto.randomUUID());
  return response;
}

For static security headers (like X-Content-Type-Options or a Content Security Policy) that apply to every route, the headers() option in next.config.ts is simpler. Proxy is the right place for per-request headers, such as a CSP with a nonce.

Geolocation and IP addresses

request.geo and request.ip were removed in Next.js 15. Your hosting provider exposes this data instead. On Vercel, use the @vercel/functions (opens in a new tab) package:

import { geolocation, ipAddress } from '@vercel/functions';

export function proxy(request: NextRequest) {
  const { country } = geolocation(request);
  const ip = ipAddress(request);
  // ...
}

Rate limiting

In-memory counters (a Map in module scope) don't work reliably on serverless platforms, because each instance has its own memory. Use a shared store such as Redis via Upstash Ratelimit (opens in a new tab) or your platform's built-in firewall rules.

Matcher patterns

export const config = {
  // Run on everything except static files, images and the favicon
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)'],
};

config can only have one matcher key, either a string or an array.

Best practices

  1. Keep proxy fast and lightweight; it runs on every matching request.
  2. Always define a matcher.
  3. Do optimistic checks in proxy, real authorization in your data layer.
  4. Use next.config for static redirects and headers.
  5. Test logged-in, logged-out and expired-session paths.

Prompting your agent

This is Next.js 16: use proxy.ts with an exported `proxy` function, not middleware.ts.
Add an optimistic auth redirect for /dashboard/* based on the session cookie, AND add
verifySession() in lib/dal.ts that every server-side data read calls. Explain where
each check happens.

More templates in our prompting guide. For the official reference, see the proxy docs (opens in a new tab).

Next steps

  • #Next.js
  • #Proxy
  • #Middleware
  • #Authentication
  • #Security

Keep reading