All posts
nextjsmiddleware

Next.js Middleware: A Practical Guide for Full-Stack Developers

A practical guide to Next.js Middleware — setup, core concepts, common mistakes, and production tips for full-stack developers.

SR

Suhail Roushan

August 6, 2026

·
6 min read
·
0 views

Next.js Middleware runs before a request is completed, letting you intercept and rewrite requests, redirect users, or add headers—all at the edge. This guide covers what Middleware is, when it's the right tool, and how to avoid the mistakes that trip up full-stack developers.

Next.js Middleware sits in that sweet spot between your server and the browser, executing code before a route renders. I've used it to handle auth checks, A/B testing, and bot detection without touching a single API route. But it's not a silver bullet—knowing when to skip it is just as important as knowing when to use it.

Why Next.js Middleware Matters (and When to Skip It)

Middleware gives you request-time control without the latency of a full server round-trip. Because it runs at the edge, it's fast—typically single-digit milliseconds. That makes it ideal for checks that need to happen on every request but don't need database access.

Here's my take: use Middleware for cheap, stateless logic. Auth token validation, geo-based redirects, header manipulation—these are perfect. Skip it when you need to query a database or fetch heavy data. That belongs in Server Components or API routes where you have proper caching and connection pooling.

The trap I see developers fall into is treating Middleware like a mini-backend. It's not. You're constrained by the Edge Runtime, which means no Node.js APIs, no database drivers, and no fs access. Respect those boundaries.

Getting Started with Next.js Middleware

The setup is minimal. Create a middleware.ts file at your project root (or inside src/ if you use that structure):

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

export function middleware(request: NextRequest) {
  // Check for a session cookie
  const session = request.cookies.get('session');
  
  if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  
  return NextResponse.next();
}

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

That's it. The matcher config tells Next.js which routes trigger the Middleware. Run npm run dev and test it—redirects happen instantly.

Core Next.js Middleware Concepts Every Developer Should Know

1. Rewrites vs. Redirects

Rewrites keep the URL in the browser while serving different content. Redirects change the URL entirely. This distinction matters for SEO and user experience.

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  
  // Redirect: user sees /new-url
  if (pathname === '/old-page') {
    return NextResponse.redirect(new URL('/new-page', request.url));
  }
  
  // Rewrite: user stays on /blog/slug, but gets /blog/[slug] content
  if (pathname.startsWith('/blog')) {
    const slug = pathname.replace('/blog/', '');
    return NextResponse.rewrite(new URL(`/blog/${slug}`, request.url));
  }
  
  return NextResponse.next();
}

2. Request Headers and Cookies

You can read and modify both at the edge. This is how you handle auth, feature flags, or locale detection.

export function middleware(request: NextRequest) {
  // Read cookie
  const theme = request.cookies.get('theme')?.value;
  
  // Set response header
  const response = NextResponse.next();
  response.headers.set('x-theme', theme || 'light');
  
  // Set a cookie on the response
  response.cookies.set('visited', 'true', { maxAge: 3600 });
  
  return response;
}

3. Conditional Logic with the next() Method

NextResponse.next() is your escape hatch. It lets the request continue unchanged, but you can still modify headers or cookies before passing it along.

export function middleware(request: NextRequest) {
  // Block requests from known bad IPs
  const blockedIPs = ['123.123.123.123'];
  const ip = request.ip;
  
  if (ip && blockedIPs.includes(ip)) {
    return new NextResponse('Access Denied', { status: 403 });
  }
  
  return NextResponse.next();
}

Common Next.js Middleware Mistakes and How to Fix Them

1. Matching Too Many Routes

I've seen developers set matcher: ['/:path*'] and then wonder why every API call is slow. Middleware runs on every matched route, so be surgical.

Fix: Use specific matchers. ['/dashboard/:path*'] covers only dashboard routes. If you need multiple, list them explicitly.

2. Doing Heavy Work in Middleware

Calling a database, parsing large JSON, or running expensive crypto operations in Middleware will tank your performance. Remember, this runs on every request.

Fix: Keep it light. If you need heavy data, do it in a Server Component or API route. Use Middleware for pre-checks only.

3. Forgetting the Edge Runtime Constraint

Trying to use fs, path, or a database driver in Middleware throws cryptic errors. The Edge Runtime is a different beast from Node.js.

Fix: Check your imports. If a package uses Node.js APIs, it won't work in Middleware. Use jose for JWT verification instead of jsonwebtoken, which relies on Node's crypto module.

When Should You Use Next.js Middleware?

Use Next.js Middleware when you need request-time logic that's fast and stateless. The sweet spots are:

  • Authentication checks — verify a JWT or session cookie before rendering protected routes
  • A/B testing — assign users to variants based on cookies or geo
  • Bot detection — check user-agent headers and block known bots
  • Geo-based redirects — serve locale-specific content or redirect to regional subdomains
  • Header injection — add security headers or cache-control directives

Skip Middleware when you need database access, heavy computation, or Node.js-specific APIs. Those belong in Server Components, Route Handlers, or external services.

Next.js Middleware in Production

1. Use Edge Runtime for Faster Cold Starts

Deploy to platforms that support the Edge Runtime (Vercel, Cloudflare Workers, Netlify). You get global distribution and sub-50ms cold starts. If you're self-hosting, stick with Node.js runtime but be aware of the latency.

2. Add Observability Early

Log Middleware decisions. In production, you need to know why a user got redirected or blocked. Add a simple logger:

export function middleware(request: NextRequest) {
  const start = Date.now();
  
  // ... your logic ...
  
  console.log(`${request.method} ${request.nextUrl.pathname} - ${Date.now() - start}ms`);
}

3. Version Your Matchers

If you're doing A/B testing or rolling out feature flags, version your matcher patterns. This makes it trivial to roll back changes without a full deploy.

Here's a complete production-ready example combining everything:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  
  // Skip auth for public routes
  if (pathname.startsWith('/api/public') || pathname === '/login') {
    return NextResponse.next();
  }
  
  // Verify JWT
  const token = request.cookies.get('token')?.value;
  
  if (!token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('next', pathname);
    return NextResponse.redirect(loginUrl);
  }
  
  try {
    await jwtVerify(token, secret);
  } catch {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  
  const response = NextResponse.next();
  response.headers.set('x-auth-time', String(Date.now()));
  
  return response;
}

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

One actionable takeaway: start with Middleware for auth checks and header injection only. Once you've got that running smoothly, expand into A/B testing and geo-routing. Keep it minimal, keep it fast, and always measure the performance impact before scaling it across your app.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch