Next.js Middleware

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

TL;DR

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

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.

Tips

  1. Use the <code>matcher</code> config to narrow which routes trigger middleware, since it runs before every request and impacts performance.
  2. Next.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. Middleware runs for every request, so expensive operations will slow your app down — keep logic fast and simple.
  2. Middleware also runs on every matched prefetch, not just full navigations, so expensive checks can fire far more often than you expect.

FAQ