Next.js Middleware

Use Next.js middleware for redirects, auth checks, headers, and request rewriting in the App Router.

TL;DR

  1. 01Create a middleware.ts file at the project root to intercept requests.
  2. 02Use middleware to check auth, redirect, or modify headers.
  3. 03Middleware runs in the Edge Runtime — keep logic fast, Node.js built-ins are unavailable.

Tips

  1. 01Use the <code>matcher</code> config to narrow which routes trigger middleware, since it runs before every request and impacts performance.
  2. 02Next.js 16 renames <code>middleware.ts</code> to <code>proxy.ts</code> going forward — the old filename still works but is deprecated, so plan to migrate.

Warnings

  1. 01Middleware runs for every request, so expensive operations will slow your app down — keep logic fast and simple.
  2. 02Middleware also runs on every matched prefetch, not just full navigations, so expensive checks can fire far more often than you expect.

File and Runtime Setup

  • Want to run logic before every route renders, with zero changes to your pages? Create one middleware.ts file at the root of your project, next to the app directory.
    // middleware.ts
    import { NextRequest, NextResponse } from "next/server";
    
    export function middleware(request: NextRequest) {
      return NextResponse.next();
    }
  • Middleware runs on every request before it reaches your routes.
  • Use the matcher export to specify which paths trigger the middleware.
    export const config = {
      matcher: ['/api/:path*', '/admin/:path*']
    };
  • Return NextResponse.next() to continue to the next handler.
  • Middleware runs in the Edge Runtime — a lightweight V8-based environment, not full Node.js. Most Node.js built-in modules (fs, path, crypto) are unavailable.

Authentication Checks

  • Skip writing auth checks into every protected page — check for a token in cookies once, in middleware, and redirect if it's missing.
    export function middleware(request: NextRequest) {
      const token = request.cookies.get('auth')?.value;
      if (!token && request.nextUrl.pathname.startsWith('/admin')) {
        return NextResponse.redirect(new URL('/login', request.url));
      }
      return NextResponse.next();
    }
  • Verify JWT tokens or check session validity before route access.
  • Redirect unauthenticated users to a login page automatically.
  • Use middleware to protect entire route segments without code in each page.
  • Combine with headers to check authentication across the app.

Redirects and Rewrites

  • A redirect changes the URL bar, a rewrite doesn't — send users to a different URL using NextResponse.redirect() when you want that visible change.
    if (request.nextUrl.pathname === '/old-page') {
      return NextResponse.redirect(new URL('/new-page', request.url));
    }
  • Rewrite the request internally without changing the user's URL.
    if (request.nextUrl.pathname.startsWith('/api')) {
      return NextResponse.rewrite(new URL('/api-v2/handler', request.url));
    }
  • Use redirects for public URL changes and SEO-friendly migrations.
  • Use rewrites to hide internal routing or proxy external APIs.
  • Both preserve the original URL in the browser when rewriting.

Custom Headers

  • Need a security header on every single response without touching each route handler? Add or modify headers once in middleware.
    export function middleware(request: NextRequest) {
      const response = NextResponse.next();
      response.headers.set('X-Custom-Header', 'value');
      return response;
    }
  • Add security headers like CORS, CSP, or rate-limiting headers.
  • Read incoming headers to customize behavior based on the request.
    const userAgent = request.headers.get('user-agent');
  • Middleware runs before routes see the request, so headers apply everywhere.
  • Use response headers to control caching and security policies.

Geolocation and Advanced Patterns

  • Middleware can branch on more than the URL path — check locale or country from the request and redirect to the right version.
    const locale = request.headers.get('accept-language')?.split(',')[0].split('-')[0];
    if (locale === 'es') {
      return NextResponse.redirect(new URL('/es' + request.nextUrl.pathname, request.url));
    }
  • request.geo and request.ip were removed from NextRequest in Next.js 15 — use the geolocation() helper from @vercel/functions to route based on the user's country instead.
    import { geolocation } from '@vercel/functions';
    
    export function middleware(request: NextRequest) {
      const { country } = geolocation(request);
      if (country === 'DE') {
        return NextResponse.redirect(new URL('/de', request.url));
      }
    }
  • Rate limit requests based on IP address or user ID — use ipAddress() from @vercel/functions now that request.ip is gone.
    import { ipAddress } from '@vercel/functions';
    
    const ip = ipAddress(request) ?? request.headers.get('x-forwarded-for') ?? 'unknown';
    // Check rate limit store keyed by ip...
  • Combine multiple checks in sequence for layered security.
  • Keep middleware logic fast — it runs on the Edge Runtime before every matched request and adds latency proportional to its execution time.

FAQ