React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

middlewarematchersredirectsrewritesheadersauthenticationnext-response

Middleware

Middleware runs before every matched request, enabling redirects, rewrites, header modifications, and authentication checks at the edge.

Recipe

Quick-reference recipe card — copy-paste ready.

// middleware.ts (root of project, same level as app/)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
export function middleware(request: NextRequest) {
  // Redirect, rewrite, or modify headers
  return NextResponse.next();
}
 
export const config = {
  matcher: ["/dashboard/:path*", "/api/:path*"],
};

When to reach for this: Authentication guards, locale detection, A/B testing, redirects, and any request-level logic that must run before rendering.

Working Example

// middleware.ts — Auth guard with locale detection
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
const protectedPaths = ["/dashboard", "/settings", "/billing"];
const defaultLocale = "en";
const supportedLocales = ["en", "fr", "de", "es"];
 
export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
 
  // 1. Locale detection — redirect if no locale prefix
  const pathnameHasLocale = supportedLocales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );
 
  if (!pathnameHasLocale) {
    const locale =
      request.headers.get("accept-language")?.split(",")[0]?.split("-")[0] ?? defaultLocale;
    const detectedLocale = supportedLocales.includes(locale) ? locale : defaultLocale;
 
    return NextResponse.redirect(
      new URL(`/${detectedLocale}${pathname}`, request.url)
    );
  }
 
  // 2. Auth check — redirect unauthenticated users
  const isProtected = protectedPaths.some((path) =>
    pathname.includes(path)
  );
  const token = request.cookies.get("session-token")?.value;
 
  if (isProtected && !token) {
    const loginUrl = new URL("/en/login", request.url);
    loginUrl.searchParams.set("callbackUrl", pathname);
    return NextResponse.redirect(loginUrl);
  }
 
  // 3. Add custom headers
  const response = NextResponse.next();
  response.headers.set("x-pathname", pathname);
  return response;
}
 
export const config = {
  matcher: [
    // Match all paths except static files and Next.js internals
    "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
  ],
};
// middleware.ts — Simple redirect map
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
const redirects: Record<string, string> = {
  "/old-blog": "/blog",
  "/docs/v1": "/docs",
  "/legacy-api": "/api/v2",
};
 
export function middleware(request: NextRequest) {
  const redirect = redirects[request.nextUrl.pathname];
  if (redirect) {
    return NextResponse.redirect(new URL(redirect, request.url), 301);
  }
  return NextResponse.next();
}
 
export const config = {
  matcher: ["/old-blog", "/docs/v1", "/legacy-api"],
};

Deep Dive

How It Works

  • Middleware runs on every matched request before the route handler or page renders. It executes at the Edge Runtime by default.
  • There is only one middleware.ts file per project. It must be at the root (next to app/ or src/). You cannot have per-route middleware files.
  • The matcher config filters which routes trigger middleware. Without a matcher, middleware runs on every request including static assets.
  • NextResponse.next() continues to the matched route. You can modify request/response headers while passing through.
  • NextResponse.redirect(url) sends a redirect response (302 by default, pass 301 for permanent).
  • NextResponse.rewrite(url) serves a different route's content without changing the browser URL.
  • Middleware runs in the Edge Runtime. Node.js APIs like fs, path, and most npm packages are unavailable. Use only Web APIs.
  • Middleware can read and set cookies. Use request.cookies.get() and response.cookies.set() for session management.
  • Middleware cannot render React components. It operates at the HTTP level, before any React rendering occurs.

Variations

// Rewrite-based A/B testing
export function middleware(request: NextRequest) {
  const bucket = request.cookies.get("ab-bucket")?.value;
  const response = NextResponse.next();
 
  if (!bucket) {
    const newBucket = Math.random() > 0.5 ? "a" : "b";
    response.cookies.set("ab-bucket", newBucket, { maxAge: 60 * 60 * 24 * 30 });
  }
 
  const currentBucket = bucket ?? "a";
  if (request.nextUrl.pathname === "/pricing") {
    return NextResponse.rewrite(
      new URL(`/pricing/${currentBucket}`, request.url)
    );
  }
 
  return response;
}
// Rate limiting with headers
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith("/api/")) {
    const ip = request.headers.get("x-forwarded-for") ?? "unknown";
    // Check rate limit (using external store like Upstash Redis)
    // This is pseudocode — use @upstash/ratelimit in production
    const response = NextResponse.next();
    response.headers.set("X-RateLimit-Limit", "100");
    return response;
  }
  return NextResponse.next();
}
// Matcher patterns
export const config = {
  matcher: [
    "/dashboard/:path*",          // /dashboard and all sub-paths
    "/api/:path*",                // All API routes
    "/blog/:slug",                // Single dynamic segment
    "/((?!_next|static|api).*)",  // Everything except _next, static, api
  ],
};

TypeScript Notes

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
// Middleware function signature
export function middleware(request: NextRequest): NextResponse | Response | undefined;
 
// NextRequest extends the Web Request API
// Key properties:
//   request.nextUrl      — parsed URL with pathname, searchParams
//   request.cookies      — RequestCookies instance
//   request.headers      — standard Headers object
//   request.geo          — { city, country, region } (Vercel only)
//   request.ip           — client IP (Vercel only)
 
// Config type
export const config: {
  matcher: string | string[];
};

Gotchas

  • Only one middleware file per project. If you need different logic for different routes, use conditional checks on request.nextUrl.pathname inside a single middleware function.
  • Middleware runs on the Edge Runtime. You cannot use Node.js-specific APIs. Libraries like bcrypt, fs, or database drivers will not work — use edge-compatible alternatives.
  • Middleware runs on static assets too unless you use a matcher. Always configure a matcher to avoid unnecessary execution.
  • Infinite redirect loops are easy to create. If you redirect /login to /en/login and middleware also matches /en/login, you get a loop. Exclude destination paths from the matcher.
  • NextResponse.next() does not short-circuit. Other middleware logic after it still runs. Return early if needed.
  • Cookies set in middleware are available in Server Components via cookies() from next/headers.
  • Rewrites are invisible to the client. The browser URL stays the same, but the server renders a different route. This can confuse client-side navigation if not handled carefully.
  • Middleware cannot access the request body. It operates on headers, cookies, and URL only.

Alternatives

ApproachWhen to Use
next.config.js redirectsStatic redirect rules that do not need runtime logic
next.config.js rewritesStatic rewrite rules without conditional logic
Route-level redirect()Redirecting inside a Server Component after data checks
Server Actions with authProtecting mutations instead of routes
Layout-level auth guardsChecking auth in a layout and redirecting

Real-World Example

From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).

// Production example: Middleware with rewrites, redirects, session refresh, and admin protection
// File: middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { updateSession } from '@/utils/supabase/middleware';
 
const legacyRedirects: Record<string, string> = {
  '/old-docs': '/docs',
  '/blog/legacy-post': '/articles',
  '/pricing-old': '/pricing',
};
 
export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
 
  // 1. Docs rewrite: serve /docs/[section] from a different internal route
  if (pathname.startsWith('/docs/')) {
    const url = request.nextUrl.clone();
    url.pathname = `/docs-internal${pathname.replace('/docs', '')}`;
    return NextResponse.rewrite(url);
  }
 
  // 2. Legacy redirects (301 permanent)
  const redirect = legacyRedirects[pathname];
  if (redirect) {
    return NextResponse.redirect(new URL(redirect, request.url), 301);
  }
 
  // 3. Session refresh - keep Supabase auth session alive
  const response = await updateSession(request);
 
  // 4. Admin route protection
  if (pathname.startsWith('/admin')) {
    const session = request.cookies.get('sb-access-token')?.value;
    if (!session) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }
 
  return response;
}
 
export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

What this demonstrates in production:

  • Execution order matters. Rewrites run first, then legacy redirects, then session refresh, then admin protection. If you check auth before the rewrite, the user might hit the wrong route.
  • The 301 status code on legacy redirects tells search engines the move is permanent. Using the default 302 would preserve the old URL in search indexes.
  • updateSession(request) is a Supabase utility that refreshes the auth session cookie on every request. This prevents the session from expiring while the user is actively browsing.
  • The matcher regex /((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*) excludes static assets and images from middleware processing. Without this, middleware runs on every image and CSS file request, adding unnecessary latency.
  • Admin protection checks for a cookie rather than making a database call. Middleware runs on the Edge Runtime where database drivers are unavailable. Full authorization checks happen in Server Components or API routes.
  • There is only one middleware.ts file per project. All route-specific logic must be handled with conditional checks on pathname.

FAQs

Where does the middleware.ts file go?

At the project root, next to the app/ or src/ directory. There is only one middleware.ts file per project. You cannot have per-route middleware files.

What runtime does middleware use and what are the limitations?
  • Middleware runs on the Edge Runtime
  • Node.js APIs like fs, path, and most npm packages are unavailable
  • You can only use Web APIs
  • You cannot access the request body
  • You cannot render React components
What happens if you do not configure a matcher?

Middleware runs on every request, including static assets like images, CSS, and JavaScript files. Always configure a matcher to avoid unnecessary execution.

Gotcha: How do you avoid infinite redirect loops in middleware?

If you redirect /login to /en/login and middleware also matches /en/login, you get an infinite loop. Exclude destination paths from the matcher or add conditional checks to skip already-processed paths.

What is the difference between NextResponse.redirect and NextResponse.rewrite?
  • redirect() sends a redirect response (302 by default, 301 for permanent) and changes the browser URL
  • rewrite() serves a different route's content without changing the browser URL
How do you read and set cookies in middleware?
export function middleware(request: NextRequest) {
  // Read a cookie
  const token = request.cookies.get("session")?.value;
 
  // Set a cookie on the response
  const response = NextResponse.next();
  response.cookies.set("visited", "true", { maxAge: 3600 });
  return response;
}
Are cookies set in middleware available in Server Components?

Yes. Cookies set in middleware are available via cookies() from next/headers in Server Components.

How do you handle different logic for different routes in a single middleware file?

Use conditional checks on request.nextUrl.pathname inside the middleware function, since you can only have one middleware file.

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  if (pathname.startsWith("/api/")) { /* API logic */ }
  if (pathname.startsWith("/admin")) { /* admin logic */ }
  return NextResponse.next();
}
What is the TypeScript signature for the middleware function?
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
export function middleware(
  request: NextRequest
): NextResponse | Response | undefined;
 
export const config: {
  matcher: string | string[];
};
What properties are available on NextRequest?
  • request.nextUrl -- parsed URL with pathname and searchParams
  • request.cookies -- RequestCookies instance
  • request.headers -- standard Headers object
  • request.geo -- city, country, region (Vercel only)
  • request.ip -- client IP (Vercel only)
Gotcha: Does NextResponse.next() short-circuit the middleware function?

No. Code after NextResponse.next() still runs. If you want to stop processing, you must explicitly return the response.

When should you use middleware redirects vs. next.config.js redirects?
  • Use next.config.js redirects for static rules that do not need runtime logic
  • Use middleware redirects when you need conditional logic based on cookies, headers, or other request data