intermediate Step 11 of 15

Middleware

Next.js Development

Middleware

Next.js middleware runs before a request is completed, allowing you to modify the response by rewriting, redirecting, setting headers, or even returning a response directly. Middleware runs on the Edge Runtime, which means it executes close to your users at CDN edge locations for minimal latency. Common use cases include authentication checks, internationalization, A/B testing, feature flags, bot detection, and request logging. Middleware is defined in a single middleware.ts file at the root of your project and can be configured to run on specific paths.

Basic Middleware

// middleware.ts (at the project root, next to app/ or pages/)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // Log all requests
  console.log(`[${request.method}] ${request.nextUrl.pathname}`);

  // Add custom headers to the response
  const response = NextResponse.next();
  response.headers.set("x-request-id", crypto.randomUUID());
  response.headers.set("x-request-path", request.nextUrl.pathname);

  return response;
}

// Configure which paths the middleware runs on
export const config = {
  matcher: [
    // Match all paths except static files and API routes
    "/((?!_next/static|_next/image|favicon.ico).*)",
  ],
};

Authentication Middleware

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// Paths that require authentication
const protectedPaths = ["/dashboard", "/settings", "/profile", "/admin"];

// Paths that should redirect authenticated users (e.g., login page)
const authPaths = ["/login", "/register"];

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const token = request.cookies.get("auth-token")?.value;
  const isAuthenticated = !!token;

  // Protect authenticated routes
  if (protectedPaths.some((path) => pathname.startsWith(path))) {
    if (!isAuthenticated) {
      const loginUrl = new URL("/login", request.url);
      loginUrl.searchParams.set("redirect", pathname);
      return NextResponse.redirect(loginUrl);
    }
  }

  // Redirect authenticated users away from auth pages
  if (authPaths.some((path) => pathname.startsWith(path))) {
    if (isAuthenticated) {
      return NextResponse.redirect(new URL("/dashboard", request.url));
    }
  }

  // Role-based access control
  if (pathname.startsWith("/admin")) {
    // Decode token to check role (simplified example)
    try {
      const payload = JSON.parse(atob(token!.split(".")[1]));
      if (payload.role !== "admin") {
        return NextResponse.redirect(new URL("/unauthorized", request.url));
      }
    } catch {
      return NextResponse.redirect(new URL("/login", request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*", "/profile/:path*",
            "/admin/:path*", "/login", "/register"],
};

Redirects and Rewrites

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Redirect old URLs to new ones
  if (pathname === "/old-blog") {
    return NextResponse.redirect(new URL("/blog", request.url), 301);
  }

  // Rewrite URL (URL stays the same, but serves different content)
  if (pathname.startsWith("/api/v1/")) {
    const newPath = pathname.replace("/api/v1/", "/api/v2/");
    return NextResponse.rewrite(new URL(newPath, request.url));
  }

  // Geolocation-based routing
  const country = request.geo?.country || "US";
  if (pathname === "/" && country === "DE") {
    return NextResponse.rewrite(new URL("/de", request.url));
  }

  // A/B testing with cookies
  if (pathname === "/pricing") {
    const variant = request.cookies.get("ab-variant")?.value;
    if (!variant) {
      const newVariant = Math.random() < 0.5 ? "a" : "b";
      const response = NextResponse.rewrite(
        new URL(`/pricing/${newVariant}`, request.url)
      );
      response.cookies.set("ab-variant", newVariant, { maxAge: 86400 });
      return response;
    }
    return NextResponse.rewrite(new URL(`/pricing/${variant}`, request.url));
  }

  return NextResponse.next();
}

Rate Limiting

// Simple rate limiting with middleware
const rateLimit = new Map<string, { count: number; timestamp: number }>();

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith("/api/")) {
    const ip = request.headers.get("x-forwarded-for") || "unknown";
    const now = Date.now();
    const windowMs = 60 * 1000; // 1 minute window
    const maxRequests = 100;

    const record = rateLimit.get(ip);
    if (record && now - record.timestamp < windowMs) {
      if (record.count >= maxRequests) {
        return NextResponse.json(
          { error: "Too many requests" },
          { status: 429 }
        );
      }
      record.count++;
    } else {
      rateLimit.set(ip, { count: 1, timestamp: now });
    }
  }

  return NextResponse.next();
}
Tip: Keep middleware lightweight since it runs on every matched request. The Edge Runtime has limitations — no Node.js APIs like fs or heavy npm packages. For complex logic, have middleware make a quick check (like reading a cookie) and defer heavy processing to API routes or Server Components.

Key Takeaways

  • Middleware runs before requests are completed, enabling authentication, redirects, and header modification.
  • It executes on the Edge Runtime for minimal latency, close to users globally.
  • Use the config.matcher export to specify which paths middleware applies to.
  • NextResponse.redirect changes the URL; NextResponse.rewrite serves different content at the same URL.
  • Keep middleware lightweight — defer heavy processing to API routes or Server Components.