Next.js Middleware: Use Cases, Examples, and Best Practices

⏱︎

Read time:

4–6 minutes

Middleware is one of the most underused parts of Next.js, yet it’s one of the most powerful. It lets you run code before a request completes — before a page renders, before an API route executes — giving you a single, centralized place to handle authentication, redirects, logging, and more. This guide walks through what Next.js Middleware actually does, real-world use cases, working code examples, and the best practices that keep it fast and maintainable as your app grows.

What Is Next.js Middleware?

Middleware in Next.js is a function that intercepts a request before it reaches a route, letting you inspect, modify, redirect, or reject it. It runs on the Edge Runtime by default, which means it executes close to the user geographically and starts up faster than a traditional Node.js server function — both of which matter for response times at scale.

You define middleware in a single middleware.ts (or .js) file at the root of your project (or inside src/), and it applies to every request unless you scope it with a matcher config. Unlike per-route logic scattered across API handlers and pages, middleware gives you one place to enforce rules across your entire app.

ts

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

export function middleware(request: NextRequest) {
  return NextResponse.next()
}

export const config = {
  matcher: '/dashboard/:path*',
}

That matcher config is important — without it, middleware runs on every single request, including static assets, which wastes execution time and can slow down unrelated routes.

Common Use Cases for Middleware

1. Authentication and Authorization The single most common use case. Middleware can check for a valid session cookie or token before allowing access to protected routes, redirecting unauthenticated users to a login page instantly — without ever rendering the protected page’s components.

ts

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

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

2. A/B Testing and Feature Flags Middleware can read a cookie or randomly assign a user to a test group, then rewrite the request to a different version of a page — all before any rendering happens, so there’s no flash of the wrong variant.

3. Geolocation-Based Redirects Using the built-in geo data available on the request, you can redirect users to region-specific content or pricing pages, which is especially useful for e-commerce and SaaS products with localized offerings.

4. Bot Detection and Rate Limiting Middleware is a natural place to inspect user-agent headers or IP addresses and block known bad actors or apply basic rate limiting before a request reaches your application logic or database.

5. Localization (i18n) Routing Detecting a user’s preferred language from the Accept-Language header and redirecting them to the correct locale-prefixed route (/en/, /es/, /fr/) is a classic middleware pattern for internationalized sites.

6. Logging and Analytics Because middleware sees every request, it’s a convenient place to log request metadata, forward events to an analytics service, or attach custom headers for downstream observability tools.

7. Maintenance Mode A single middleware check against an environment variable can redirect all traffic to a maintenance page during a deploy or incident, without touching individual routes.

Middleware Best Practices

  • Keep it lightweight. Middleware runs on the Edge Runtime, which has a smaller API surface than full Node.js — avoid heavy computation, large dependencies, or anything that isn’t strictly request-routing logic.
  • Scope with matcher aggressively. Don’t let middleware run on static assets, images, or routes that don’t need it. A tight matcher config directly improves latency across your whole app.
  • Avoid direct database calls. The Edge Runtime doesn’t support every Node.js API, and adding a database round-trip to every matched request adds latency to routes that may not need it. Prefer lightweight checks (cookies, headers, JWTs) over full data lookups.
  • Fail safely. If a check in middleware throws an unexpected error, make sure your fallback behavior doesn’t accidentally lock out all users or expose protected content — wrap risky logic in try/catch with a sensible default.
  • Don’t duplicate logic across middleware and route handlers. If both are checking authentication in slightly different ways, they can drift out of sync. Middleware should generally handle the coarse-grained gatekeeping; route handlers can still do fine-grained checks.
  • Test locally with realistic matchers. Middleware behaves differently across environments (local dev vs. edge deployment), so test matcher patterns against real routes before shipping, especially for regex-based matchers.
  • Log sparingly. Since middleware runs on every matched request, verbose logging can quickly become expensive or noisy at scale — log only what you’ll actually use.

Middleware vs. Route Handlers vs. Server Actions

It’s easy to conflate these, so here’s the distinction:

  • Middleware runs before routing is resolved — it can redirect, rewrite, or block a request before any page or API route executes.
  • Route Handlers (app/api/.../route.ts) are full backend endpoints that handle specific HTTP methods and return responses, similar to an Express route.
  • Server Actions are functions callable directly from components (often forms) that run on the server, typically used for mutations rather than request gatekeeping.

Middleware is the right tool when the decision needs to happen for every request to a set of routes, before anything else runs — authentication gates, redirects, and locale detection are the clearest examples.

Next.js Middleware gives you a single, centralized layer to control routing decisions — authentication, redirects, localization, and more — before a page or API route ever executes. Used well, it keeps cross-cutting logic out of individual pages and components, runs close to the user for minimal latency, and scales cleanly as an app grows. Used carelessly (unscoped matchers, heavy computation, database calls), it can just as easily become a hidden performance bottleneck. Keep it lightweight, scope it precisely, and it becomes one of the most reliable tools in a Next.js developer’s toolkit.

Leave a Reply

Your email address will not be published. Required fields are marked *