Next.js Proxy and Middleware

Run code before a request completes with proxy.ts, the Next.js 16 replacement for middleware.

TL;DR

  1. Create proxy.ts at the project root and export a proxy function.
  2. Limit where it runs with the matcher option in config.
  3. Return NextResponse.redirect() or NextResponse.rewrite() to change the request.

File and Matcher

    proxy.ts

    Export a function named proxy that runs before matched requests complete.

    // proxy.ts
    import { NextResponse } from 'next/server';
    import type { NextRequest } from 'next/server';
    
    export function proxy(request: NextRequest) {
      return NextResponse.next();
    }
    matcher

    Limit the proxy to specific paths by exporting a config object.

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

    Skip API routes and static assets with a pattern that excludes them.

    export const config = {
      matcher: [
        '/((?!api|_next/static|_next/image).*)',
      ],
    };
    middleware-to-proxy

    Rename an existing middleware.ts file and its function with the official codemod.

    npx @next/codemod@canary middleware-to-proxy .

Redirects and Rewrites

    NextResponse.redirect()

    Send the browser to a different URL, which changes the address bar.

    if (request.nextUrl.pathname === '/old') {
      return NextResponse.redirect(
        new URL('/new', request.url)
      );
    }
    NextResponse.rewrite()

    Serve a different route internally while the visible URL stays the same.

    if (request.nextUrl.pathname.startsWith('/docs')) {
      return NextResponse.rewrite(
        new URL('/help', request.url)
      );
    }
    Header Redirect

    Redirect based on a request header, such as the browser's preferred language.

    const lang = request.headers
      .get('accept-language')
      ?.slice(0, 2);
    
    if (lang === 'es') {
      return NextResponse.redirect(
        new URL('/es', request.url)
      );
    }

Headers and Cookies

    Response Headers

    Add a security header to every matched response in one place.

    const response = NextResponse.next();
    response.headers.set('X-Frame-Options', 'DENY');
    return response;
    Request Headers

    Pass a value to your pages by adding a header to the forwarded request.

    const headers = new Headers(request.headers);
    headers.set('x-pathname', request.nextUrl.pathname);
    
    return NextResponse.next({
      request: { headers },
    });
    Cookies

    Read cookies from the request and set new ones on the response.

    const theme = request.cookies.get('theme')?.value;
    
    const response = NextResponse.next();
    response.cookies.set('visited', 'true');
    return response;

Auth Checks

    Session Redirect

    Redirect to the login page when the session cookie is missing.

    export function proxy(request: NextRequest) {
      const session = request.cookies.get('session');
      if (!session) {
        return NextResponse.redirect(
          new URL('/login', request.url)
        );
      }
      return NextResponse.next();
    }
    Direct Response

    Reject an API request with a JSON error instead of a redirect.

    if (!request.headers.get('authorization')) {
      return Response.json(
        { error: 'Unauthorized' },
        { status: 401 }
      );
    }
    waitUntil()

    Finish background work such as logging after the response is sent.

    import type { NextFetchEvent } from 'next/server';
    
    export function proxy(
      request: NextRequest,
      event: NextFetchEvent
    ) {
      event.waitUntil(logVisit(request.nextUrl.pathname));
      return NextResponse.next();
    }

Tips

  1. Run npx @next/codemod@canary middleware-to-proxy . to rename the file and the exported function automatically when you upgrade.
  2. Exclude static files with a negative matcher pattern, so the proxy does not run on every image and script request.

Warnings

  1. The proxy.ts file always uses the Node.js runtime, and setting a runtime option in it throws an error.
  2. Never treat proxy.ts as your only auth check, because a matcher change can silently leave Server Functions unprotected.

In Practice

FAQ