# Next.js Middleware: What It Does and How to Use It Correctly

> A deep-dive into Next.js Middleware — how it works at the edge, what it is and is not suited for, and best practices for auth, redirects, and request rewriting.

- **Published:** 2026-01-17
- **Author:** Clixo
- **Reading time:** 6 min read
- **Tags:** next.js, middleware, edge-runtime, auth, routing
- **Canonical URL:** https://clixo.sh/blog/nextjs-middleware-guide-and-best-practices

Next.js Middleware is one of those features that looks simple and turns out to be surprisingly sharp once you deploy it at scale. It runs before a request reaches your page or API route — on the Edge Runtime, close to the user — and it can read the request, modify the response, redirect, or rewrite the destination. When used well, it handles authentication guards, locale detection, and A/B testing in a single function with near-zero latency. When misused, it adds latency to every request and creates subtle bugs that are hard to reproduce.

This guide covers what middleware actually is, what the edge runtime constrains, and the practices that keep it correct in production.

## What Middleware Is

A `middleware.ts` (or `middleware.js`) file at the root of your project exports a function that Next.js runs before completing any matched request. It has access to the incoming `Request` object and can return a `Response` — either the original (pass-through), a redirect, or a rewrite.

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

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

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

  return NextResponse.next()
}

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

This is the canonical auth guard: read the session cookie, redirect unauthenticated users to `/login`, let everyone else through.

```mermaid
sequenceDiagram
  participant C as Client
  participant M as Middleware
  participant P as Page
  C->>M: Incoming request
  M->>M: Read cookies and headers
  alt No session cookie
    M-->>C: 307 Redirect to /login
  else Session valid
    M->>P: NextResponse.next()
    P-->>C: Rendered page HTML
  end
```

## The Edge Runtime: What This Means for Your Code

Middleware runs in the Edge Runtime, not Node.js. This is a lightweight, V8-based environment that starts faster and runs globally on CDN edge nodes — but it does not have access to all Node.js APIs.

What is not available in the Edge Runtime:

- The `fs` module (no filesystem access)
- Most native Node.js modules
- Most database clients that depend on Node.js primitives

This means middleware cannot query your database directly. It can read cookies, headers, and URL parameters. For any auth check that requires a database lookup — validating a JWT against a revocation list, or looking up a user's role — you have two options: use a stateless token (JWT with claims embedded) that can be verified in the edge without a database call, or proxy the check through an API route.

## Writing a Good Matcher

The `config.matcher` field controls which routes trigger the middleware function. Without a matcher, middleware runs on every request — including `_next/static` files, images, and fonts. This is almost never what you want.

**Exclude static assets explicitly:**

```typescript
export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}
```

This pattern matches all routes except Next.js static files and common image extensions. Middleware running on static asset requests adds latency without benefit.

For auth guards, prefer matching only the routes that need protection:

```typescript
export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
}
```

## Authentication Guards

Middleware is the right place for route-level authentication guards — it prevents unauthenticated users from ever reaching a protected page, even briefly. But it is not a substitute for server-side auth checks.

A user who bypasses your redirect (for example, by disabling JavaScript or using a direct API call) could still reach the page if the page itself does not verify authentication. Middleware is a first layer; server-side verification in the page or API route is the enforcing layer.

**Best practice:** Use middleware to redirect unauthenticated users. Use server-side auth checks in protected pages and API routes to enforce access control. Neither layer alone is sufficient.

## Redirects and Rewrites

Redirects send the user to a different URL with an HTTP redirect status (307 for temporary, 308 for permanent). The browser navigates to the new URL.

Rewrites serve content from a different URL without changing what the browser displays. This is useful for:

- Proxying requests to an external API while keeping the origin hidden
- Serving different content to different user segments without a visible URL change
- A/B testing: routing a percentage of traffic to an experimental page without a redirect

```typescript
export function middleware(request: NextRequest) {
  const bucket = request.cookies.get('ab-bucket')?.value

  if (bucket === 'experiment') {
    return NextResponse.rewrite(new URL('/experiment/home', request.url))
  }

  return NextResponse.next()
}
```

The user sees `/`, but users in the `experiment` bucket are served content from `/experiment/home`.

## Locale Detection

Middleware is the standard location for locale detection and internationalization routing. Read the `Accept-Language` header, determine the user's preferred locale, and redirect or rewrite to the locale-specific route.

Next.js has built-in i18n routing support in `next.config.js`, but custom middleware gives you more control: cookie-based locale preference, URL-based detection, and fallback logic.

## What Middleware Should Not Do

**Heavy computation.** Middleware runs on every matched request. Any logic that adds meaningful latency compounds at scale. Keep it fast — cookie reads, header checks, simple redirects.

**Database queries.** The Edge Runtime cannot use most database clients. Attempts to query a database from middleware will fail at runtime or require a full network round-trip to an API route, negating the edge performance advantage.

**Business logic.** Middleware is an infrastructure layer. Routing, auth checks, and request augmentation belong here. Business rules belong in server components, Server Actions, or API routes.

**Stateful operations.** Middleware is stateless. It cannot accumulate state across requests.

## Debugging Middleware

Middleware runs differently in development (`next dev`) than in production. In development, it runs as a Node.js function with some edge simulation. In production, it runs in the actual Edge Runtime.

If your middleware works locally but breaks in production, the most common cause is a dependency that is not edge-compatible. Check your imports against the list of supported packages for the Edge Runtime.

Log strategically — `console.log` output from edge middleware appears in your deployment platform's edge function logs, not in the Next.js server logs.

---

Middleware is a clean solution to a specific set of problems: routing logic that needs to run before the page, close to the user, without a cold start. It works best when kept focused and fast.

If your team is building authentication flows, multi-tenant routing, or internationalization on Next.js and needs experienced engineering support, [talk to Clixo](https://clixo.sh/#contact). We build full-stack Next.js systems that hold up in production.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
