Next.js Middleware Tutorial with Authentication and Redirect Use Cases
Middleware has appeared several times already in this course — protecting authenticated routes in Lesson 5.4, enforcing role-based access in Lesson 5.5 — always used, but never fully explained on its own terms. This lesson steps back to cover Next.js middleware comprehensively: what it actually is, where and when it runs (specifically, at the network edge, before a request even reaches your application's rendering logic), and the full range of common use cases beyond authentication, including redirects, rewrites, and header manipulation.
Learning Objectives
- Explain what Next.js middleware is and where it executes in the request lifecycle.
- Understand the Edge Runtime middleware runs on and its implications.
- Configure a middleware.ts file with a matcher to target specific routes.
- Implement authentication checks and conditional redirects in middleware.
- Recognize other common middleware use cases beyond authentication.
Core Definitions
- Middleware: Code defined in a middleware.ts file that runs before a request completes, allowing you to modify the response by rewriting, redirecting, or altering request/response headers.
- Edge Runtime: A lightweight JavaScript execution environment (distinct from the full Node.js runtime) that Next.js middleware runs on by default, optimized for running geographically close to the requesting user.
- Matcher: A configuration option in middleware.ts specifying exactly which routes the middleware function should run for, avoiding unnecessary execution on unrelated paths.
- Redirect: An instruction telling the browser to navigate to a different URL than the one originally requested.
- Rewrite: Internally serving different content for a requested URL without changing the URL the browser actually displays.
Detailed Explanation
Middleware in Next.js is defined in a single middleware.ts file at your project's root (or inside src/ if using that convention), exporting a default function that runs before a request is allowed to reach its destination — a page, a Route Handler, anything. This positioning, before the actual rendering or handler logic runs, is what makes middleware ideal for cross-cutting concerns that should apply broadly: checking authentication, redirecting based on a visitor's location, or rewriting a URL, all before any of your application's actual page-rendering work begins.
A defining characteristic of Next.js middleware is that it runs on the Edge Runtime by default — a lightweight, fast-starting JavaScript environment distinct from the full Node.js runtime your Server Components and Route Handlers typically use, and covered in full depth in Lesson 6.7. This matters practically: the Edge Runtime supports a smaller subset of Node.js APIs (no direct file system access, for instance), and is specifically optimized to run geographically close to the requesting user across a distributed network, minimizing the latency added by every request passing through it.
By default, middleware would run on every single request to your application, which is usually unnecessary and wasteful. The matcher configuration, exported alongside your middleware function, lets you specify exactly which paths it should apply to — commonly using a pattern like `matcher: ['/dashboard/:path*', '/admin/:path*']` to scope authentication checks only to routes that actually need protection, letting requests to public marketing pages skip the middleware's logic entirely for better performance.
Within the middleware function itself, common actions include: checking for a valid session or authentication token and calling `NextResponse.redirect(new URL('/login', request.url))` if absent (exactly the pattern from Lesson 5.4); rewriting a request using `NextResponse.rewrite()` to internally serve different content without changing the visible URL (useful for A/B testing or serving different content based on a detected locale, foreshadowing Lesson 6.5's internationalization content); and reading or setting request/response headers, useful for things like adding security headers consistently across every response, or reading a visitor's geolocation data (available on the request object in supported deployment environments) to make location-based routing decisions.
Where Middleware Runs in the Next.js Request Lifecycle
{"heading":"Where Middleware Runs in the Next.js Request Lifecycle","description":"Visualize middleware's position in the request lifecycle:\n\n[Incoming Request] --> [middleware.ts runs, on the Edge Runtime]\n ├── Check matcher: does this path apply? --NO--> [Skip middleware, continue normally]\n └── YES --> [Run middleware logic]\n ├── Redirect? --> NextResponse.redirect(newUrl) --> browser navigates elsewhere\n ├── Rewrite? --> NextResponse.rewrite(newUrl) --> different content, SAME visible URL\n └── Allow through --> NextResponse.next() --> request continues to the actual page/handler"}
Next.js Practical Example
// middleware.ts — authentication check with a scoped matcher
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const sessionCookie = request.cookies.get('session');
if (!sessionCookie && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};
// A rewrite example — serving different content for the same visible URL
export function localeMiddleware(request: NextRequest) {
const country = request.geo?.country ?? 'US';
if (country === 'FR' && !request.nextUrl.pathname.startsWith('/fr')) {
return NextResponse.rewrite(new URL(`/fr${request.nextUrl.pathname}`, request.url));
}
return NextResponse.next();
}
The primary middleware function checks for a session cookie and redirects to /login if it's missing and the request targets a /dashboard path — a lightweight, edge-level check that runs before any dashboard page's own rendering logic even starts. The matcher config scopes this check to only /dashboard and /admin paths, meaning requests to the homepage or public marketing pages skip this middleware entirely, avoiding unnecessary overhead. localeMiddleware demonstrates a rewrite: a French visitor requesting a path is transparently served French-localized content at an internal /fr-prefixed path, while their browser's address bar continues to show the original URL they actually requested — the user experience feels seamless, with the locale detection and content substitution happening invisibly at the edge.
How Companies Use Middleware in Production
- E-commerce platforms use middleware to detect a visitor's country via geolocation and rewrite requests to region-specific pricing or currency pages without changing the visible URL.
- SaaS applications use middleware extensively for authentication gatekeeping, exactly as covered in Lesson 5.4/5.5, protecting entire sections of an app before any protected page's code runs.
- A/B testing platforms use middleware rewrites to serve different page variants to different visitor segments, all while keeping a single, consistent URL for analytics and sharing purposes.
- Multi-tenant SaaS products use middleware to detect a subdomain (like customer1.example.com) and rewrite the request internally to route to that specific tenant's content.
- Security-conscious applications use middleware to add consistent security headers (like Content-Security-Policy) across every single response, without needing to configure this in every individual page or Route Handler.
Common Mistakes to Avoid
- Forgetting to configure a matcher, causing middleware to run on every single request, including ones that don't need it, adding unnecessary latency.
- Attempting to use full Node.js-only APIs (like direct file system access) inside middleware, which fails since it runs on the more limited Edge Runtime.
- Confusing redirect and rewrite, using a redirect when a rewrite (preserving the visible URL) was actually the intended behavior, or vice versa.
- Relying solely on middleware for all authorization needs without the deeper, resource-specific checks covered in Lesson 5.5's defense-in-depth approach.
- Writing overly complex, slow logic inside middleware, negating its edge-optimized, low-latency design purpose.
Interview Notes
- Middleware runs before a request reaches its destination page or Route Handler, on the Edge Runtime by default.
- The matcher configuration scopes middleware to specific routes, avoiding unnecessary execution elsewhere.
- NextResponse.redirect() sends the browser to a new URL; NextResponse.rewrite() serves different content while keeping the same visible URL; NextResponse.next() allows the request through unmodified.
- The Edge Runtime supports a smaller subset of Node.js APIs than the full Node.js runtime.
- Common middleware use cases include authentication, geolocation-based content, A/B testing, multi-tenant routing, and consistent header manipulation.
Key Takeaways
- Middleware's positioning before a request reaches its destination makes it ideal for broad, cross-cutting concerns like authentication and redirects.
- Running on the Edge Runtime by default gives middleware low-latency execution, at the cost of a more limited API surface than full Node.js.
- The matcher configuration is essential for both performance and correctness, scoping middleware to exactly the routes that need it.
- Redirects and rewrites serve different purposes — changing versus preserving the visible URL — and choosing correctly matters for the intended user experience.
Summary
Next.js middleware, defined in a middleware.ts file, runs before an incoming request reaches its destination page or Route Handler, executing on the lightweight Edge Runtime by default for low-latency, geographically distributed execution. A matcher configuration scopes middleware to specific routes, avoiding unnecessary execution on unrelated paths — critical both for performance and for correctly targeting protective checks like authentication. Within middleware, NextResponse.redirect() sends the browser to a genuinely different URL, NextResponse.rewrite() serves different content while keeping the visible URL unchanged, and NextResponse.next() allows a request to proceed unmodified. Beyond the authentication use cases covered in earlier lessons, middleware commonly handles geolocation-based content, A/B testing, multi-tenant subdomain routing, and consistent security header application across every response.