Next.js Middleware

Middleware is code that runs before a request reaches your page or API route. It intercepts every incoming request, does something with it — like checking if the user is logged in — and then decides whether to continue, redirect, or block the request. Middleware runs at the edge, which means it runs as close to the user as possible, making it extremely fast.

What Middleware Can Do

REQUEST COMES IN
      ↓
MIDDLEWARE RUNS FIRST
      ↓
Possible actions:
  → Continue to the page (no change)
  → Redirect to a different URL
  → Rewrite the URL (change destination silently)
  → Add or modify headers
  → Return a response directly (block the request)

Creating the Middleware File

Create a file named middleware.js at the root of your project — at the same level as the app/ folder, not inside it.

my-app/
  app/
    page.js
  middleware.js     ← lives here, at the root
  package.json

A Basic Middleware Example

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  console.log('Request to:', request.nextUrl.pathname);
  return NextResponse.next();   // Continue to the requested page
}

NextResponse.next() passes the request through to the page. Without calling this, the request stops and the user gets no response.

Redirecting with Middleware

The most common use of middleware is protecting pages that require login. Check for an authentication token — usually stored in a cookie — and redirect unauthenticated users to the login page.

import { NextResponse } from 'next/server';

export function middleware(request) {
  const token = request.cookies.get('auth-token');
  const isLoggedIn = Boolean(token);
  const isLoginPage = request.nextUrl.pathname === '/login';

  if (!isLoggedIn && !isLoginPage) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}
DIAGRAM: Authentication Flow with Middleware

User visits /dashboard
        ↓
Middleware checks: is auth-token cookie present?
        ↓                       ↓
    YES                         NO
        ↓                       ↓
Continue to /dashboard     Redirect to /login
(page renders normally)    (user must log in)

Controlling Which Routes Middleware Runs On

By default, middleware runs on every request including image files and API routes. Use a config export with a matcher to limit which paths trigger your middleware.

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

This tells Next.js to run middleware only for URLs starting with /dashboard, /profile, or /admin. All other routes skip middleware entirely.

Matcher Patterns

'/dashboard'          → Matches exactly /dashboard
'/dashboard/:path*'   → Matches /dashboard and all sub-paths
'/api/:path*'         → Matches all /api/* routes
'/((?!_next).*)'      → Matches everything except Next.js internals

URL Rewriting

Rewriting changes which page serves a request without changing the URL the user sees. This is useful for A/B testing, internationalization, or serving different content based on a header or cookie.

import { NextResponse } from 'next/server';

export function middleware(request) {
  const country = request.geo?.country || 'US';

  if (country === 'DE') {
    // Serve the German version of the page
    return NextResponse.rewrite(new URL('/de' + request.nextUrl.pathname, request.url));
  }

  return NextResponse.next();
}
DIAGRAM: Rewrite vs Redirect

REDIRECT:
User visits /home → Middleware → Browser goes to /new-home
URL in browser changes to /new-home

REWRITE:
User visits /home → Middleware → Server serves /new-home
URL in browser stays as /home (user does not know)

Adding Headers to Requests and Responses

Middleware can add headers to the outgoing response. This is commonly used for security headers like Content Security Policy.

import { NextResponse } from 'next/server';

export function middleware(request) {
  const response = NextResponse.next();

  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');

  return response;
}

Reading Cookies and Headers in Middleware

export function middleware(request) {
  // Read a cookie
  const theme = request.cookies.get('theme')?.value;

  // Read a request header
  const userAgent = request.headers.get('user-agent');

  // Read the current path
  const path = request.nextUrl.pathname;

  // Read a search param
  const searchParams = request.nextUrl.searchParams;
  const query = searchParams.get('q');

  return NextResponse.next();
}

Middleware vs API Routes: When to Use Which

USE MIDDLEWARE FOR:
  ✓ Authentication checks (before page loads)
  ✓ Redirects based on country, device, or user role
  ✓ Adding security headers to all responses
  ✓ Rate limiting incoming requests
  ✓ A/B testing (rewrite to variant pages)

USE API ROUTES FOR:
  ✓ Processing form data
  ✓ Talking to a database
  ✓ Complex business logic
  ✓ Returning JSON data to the frontend

Key Takeaway

Middleware sits between every incoming request and your pages. Create it in middleware.js at the project root. Use it for fast, stateless tasks like checking authentication, redirecting users, adding headers, or rewriting URLs. Always use the config.matcher to limit middleware to only the routes that need it — this keeps your app fast.

Leave a Comment