TL;DR: Next.js middleware is a function that runs before every request reaches your pages or API routes. It lives in a single middleware.ts file at your project root and executes on the Edge Runtime — a lightweight environment that's fast but limited. AI uses it to add authentication checks, redirects, geo-routing, and header modifications without touching your actual page code. Think of it as a bouncer that checks every visitor before they get through the door.
Why AI Coders Need This
Here's how it happens: you're building a Next.js app with Cursor or Claude Code. Everything's working. You have a dashboard, a settings page, maybe a profile page. Then you say the magic words:
"Add auth protection so only logged-in users can see the dashboard."
AI doesn't add an if (!user) check to your dashboard page. Instead, it creates a brand-new file you've never seen before — middleware.ts — at the very root of your project. This file doesn't render anything. It doesn't have a UI. It sits there silently intercepting every single HTTP request before your pages even know someone's visiting.
If you don't understand what middleware is, this feels like AI went rogue. You asked for a simple auth check and got an invisible layer that controls your entire app's traffic. But this is actually the correct pattern. The reason AI reaches for middleware instead of page-level checks is that middleware handles auth in one place, for every route, without duplicating logic across dozens of files.
The problem? AI rarely explains what it just did. You're left with a file you didn't ask for, running code you don't understand, on a runtime you've never heard of. Let's fix that.
The Real Scenario
"Add auth protection to my Next.js app. Users who aren't logged in should be redirected to /login. Protect /dashboard, /settings, and /profile."
Simple enough request. You want three pages behind a login wall. Here's what AI generates — and it's probably not what you expected.
What AI Generated
Instead of modifying your three page components, AI creates this file at your project root:
// middleware.ts — at the root of your project
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Check if user has a session token
const token = request.cookies.get('session-token')?.value;
// If no token and trying to access protected routes, redirect to login
if (!token) {
const loginUrl = new URL('/login', request.url);
// Save where they were trying to go so we can redirect back after login
loginUrl.searchParams.set('callbackUrl', request.pathname);
return NextResponse.redirect(loginUrl);
}
// Token exists — let the request through
return NextResponse.next();
}
// Only run middleware on these routes
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*', '/profile/:path*'],
};
That's it. No changes to your dashboard page. No changes to your settings page. This one file — sitting at the project root, never imported by anything — handles auth for all three routes automatically. Every request to those paths gets intercepted before the page even starts rendering.
Let's break down exactly what each part does.
Understanding Each Part
The Matcher Config
This is the most important part to understand, and the one AI gets wrong most often:
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*', '/profile/:path*'],
};
The matcher tells Next.js: "Only run this middleware on these specific URL patterns." Without a matcher, middleware runs on every single request — including images, CSS files, fonts, favicons, and API calls. That's almost never what you want.
The :path* syntax means "this route and everything under it." So /dashboard/:path* matches /dashboard, /dashboard/analytics, /dashboard/users/123, and any other nested path.
'/dashboard' — only the exact dashboard page.
'/dashboard/:path*' — dashboard and everything nested under it.
'/(dashboard|settings|profile)/:path*' — multiple protected sections in one pattern.
'/((?!api|_next/static|_next/image|favicon.ico).*)' — everything EXCEPT API routes and static files. AI generates this a lot.
NextResponse — The Three Actions
Inside middleware, you have three main things you can do with a request:
// 1. Let the request through (do nothing)
return NextResponse.next();
// 2. Redirect the user to a different URL (browser sees the redirect)
return NextResponse.redirect(new URL('/login', request.url));
// 3. Rewrite the URL (user sees one URL, server serves a different page)
return NextResponse.rewrite(new URL('/maintenance', request.url));
next() means "I'm done, proceed normally." redirect() sends the user to a different page (the browser URL changes). rewrite() secretly serves a different page without changing the URL — the user thinks they're on /dashboard but they're actually seeing your maintenance page.
Cookies and Headers
Middleware can read and set cookies and headers on both the request (incoming) and response (outgoing):
export function middleware(request: NextRequest) {
// Read a cookie from the incoming request
const token = request.cookies.get('session-token')?.value;
const theme = request.cookies.get('theme')?.value;
// Read a header from the incoming request
const country = request.headers.get('x-vercel-ip-country');
const userAgent = request.headers.get('user-agent');
// Create a response and modify it
const response = NextResponse.next();
// Set a cookie on the outgoing response
response.cookies.set('visited', 'true', { maxAge: 60 * 60 * 24 });
// Set a header on the outgoing response
response.headers.set('x-custom-header', 'middleware-was-here');
return response;
}
This is how middleware handles things like A/B testing (set a cookie to assign users to a test group), geo-routing (read the country header and rewrite to a localized version), or analytics (add tracking headers to every response).
The Edge Runtime
Here's the part that causes the most confusion: middleware doesn't run on your normal Node.js server. It runs on the Edge Runtime — a stripped-down JavaScript environment designed to be fast and globally distributed.
Think of it this way: your regular server-side code runs in a data center somewhere. Edge functions run on CDN nodes spread across the world, closer to your actual users. This means middleware adds almost zero latency — it intercepts requests in a few milliseconds, not hundreds.
The tradeoff? The Edge Runtime is limited. You can't use many Node.js APIs. You can't access the file system. You can't use most database drivers. You can't import large npm packages that depend on native code. This is the #1 source of middleware bugs that AI creates — more on that in a moment.
Common Use Cases
Here's what middleware is actually good for — and what it's not:
| Use Case | How It Works | Best For |
|---|---|---|
| Auth Redirects | Check for session cookie → redirect to /login if missing | Protecting dashboard, admin, and account pages |
| Geo-Routing | Read country header → rewrite to localized version (/en, /fr, /de) | Multi-language sites deployed on Vercel |
| A/B Testing | Check/set cookie for test group → rewrite to variant page | Testing landing pages without client-side flicker |
| Rate Limiting | Track request count via headers/cookies → return 429 if exceeded | Basic API protection (for heavy rate limiting, use a dedicated service) |
| Bot Detection | Check user-agent and IP patterns → block or redirect suspicious traffic | Reducing spam and scraping on public endpoints |
| Request Logging | Add correlation headers, log request metadata before it hits the app | Debugging, analytics, tracing requests across services |
| Feature Flags | Read cookie/header for feature group → rewrite to feature variant | Rolling out new features to a percentage of users |
What middleware is NOT for: database queries, sending emails, processing payments, rendering HTML, returning JSON responses, or any heavy computation. Those belong in API routes or server components.
What AI Gets Wrong About Next.js Middleware
Middleware is one of those features where AI generates code that looks right, passes TypeScript checks, and then breaks in production. Here are the five most common failures:
This is the big one. AI writes middleware that imports fs, uses Buffer.from() with certain encodings, calls crypto.randomBytes(), or imports a database client like Prisma. These all fail at the Edge Runtime because it's not full Node.js. The error usually looks like "Module not found: Can't resolve 'fs'" or "Dynamic code evaluation is not allowed." Fix: tell AI "this runs on the Edge Runtime — only use Web APIs (fetch, crypto.subtle, TextEncoder, URL, Headers, Request, Response)."
AI forgets the matcher config, or uses a catch-all pattern that doesn't exclude static files. Suddenly your middleware runs on every image, every CSS file, every font request, and every _next/ internal request. Your app slows to a crawl or breaks entirely because the middleware is redirecting requests for /favicon.ico to the login page. Fix: always include a matcher. At minimum, exclude _next/static, _next/image, and favicon.ico.
The classic: middleware checks for auth, redirects to /login, but the matcher also matches /login. The login page triggers middleware, which redirects to /login, which triggers middleware, which redirects to /login — infinite loop. Your browser shows "ERR_TOO_MANY_REDIRECTS" and the app is completely broken. Fix: make sure your login page (and any public pages) are NOT included in the matcher, or add an explicit check at the top of your middleware:
// Always exclude the login page from auth checks
if (request.nextUrl.pathname.startsWith('/login')) {
return NextResponse.next();
}
AI sometimes puts business logic in middleware that belongs in API routes — like validating request bodies, querying a database to check permissions, or constructing JSON responses. Middleware is a gatekeeper, not a handler. It should make fast, lightweight decisions (does this cookie exist? which country is this request from?) and pass the request along. If AI generates middleware with await prisma.user.findUnique(), that's a red flag. Move it to an API route or server component.
Beyond the Node.js API issue, the Edge Runtime has a size limit (usually 1MB for the middleware bundle on Vercel) and an execution time limit (typically 30 seconds, but practically you should aim for under 100ms). AI sometimes generates middleware that imports heavy libraries like jsonwebtoken (which uses Node.js crypto), axios (use native fetch instead), or validation libraries that balloon the bundle size. Keep middleware lean. If it's doing too much, it shouldn't be middleware.
Debugging Middleware with AI
Middleware bugs are tricky because the file runs invisibly — you don't see it in the browser, and errors often show up as mysterious redirects or blank pages rather than clear error messages. Here's how to debug effectively:
"Here's my middleware.ts: [paste file]. When I visit /dashboard, I expect to see my dashboard page, but instead I get [redirect loop / blank page / 500 error]. My matcher config is [paste config]. What's wrong?"
You can also add temporary logging to see what middleware is doing:
export function middleware(request: NextRequest) {
// Temporary debug logging — check your terminal/Vercel logs
console.log('Middleware hit:', request.nextUrl.pathname);
console.log('Has token:', !!request.cookies.get('session-token'));
console.log('Method:', request.method);
// ... rest of your middleware logic
}
These logs show up in your terminal during development (next dev) and in your Vercel function logs in production. Remove them before deploying — middleware runs on every matched request, and excessive logging can hit your log limits fast.
"I need to [describe what you want]. Should this be in middleware.ts, an API route, a server component, or client-side code? Explain why."
This prompt saves you from putting the wrong logic in the wrong place. AI is good at explaining the boundary between middleware and other server-side patterns — it just doesn't always follow its own advice when generating code.
Testing Middleware Locally
Middleware behaves differently in development vs. production. In next dev, it runs in a simulated Edge Runtime. Some things that work locally might fail on Vercel because the real Edge Runtime is stricter. If your middleware works locally but breaks in production:
// Test with the edge runtime flag in next.config.js (Next.js 15)
/** @type {import('next').NextConfig} */
const nextConfig = {
// This doesn't change middleware behavior, but helps catch issues early
experimental: {
// Middleware is always edge — this is just a reminder
},
};
module.exports = nextConfig;
The real test: deploy to a preview branch on Vercel and test there. That's the actual Edge Runtime your middleware will run on in production.
When NOT to Use Middleware
Just because AI generates a middleware file doesn't mean you need one. Here's when to push back:
- Simple page-level auth: If you're only protecting one page, a server-side check in that page's server component is simpler and more obvious than a middleware file.
- Database-dependent decisions: If the check requires a database query (like "is this user an admin?"), do it in a server component or API route where you have full Node.js access.
- Heavy computation: Anything that takes more than a few milliseconds doesn't belong in middleware. The whole point is speed.
- Returning data: Middleware can redirect and rewrite, but it's not designed to return JSON responses or render HTML. That's what API routes and pages are for.
Ask yourself: "Does this need to happen before the page loads, for every request to this route, and can it be done without a database or heavy processing?" If yes → middleware. If no → server component, API route, or client-side code.
A Complete Real-World Middleware
Here's a production-ready middleware that combines several common patterns. This is closer to what you'd actually ship:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// Routes that don't require authentication
const publicRoutes = ['/login', '/signup', '/forgot-password', '/'];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const token = request.cookies.get('session-token')?.value;
// 1. Auth check for protected routes
if (!publicRoutes.some(route => pathname.startsWith(route)) && !token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(loginUrl);
}
// 2. Redirect logged-in users away from login page
if (pathname === '/login' && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
// 3. Add security headers to all responses
const response = NextResponse.next();
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
return response;
}
export const config = {
matcher: [
/*
* Match all paths except:
* - _next/static (static files)
* - _next/image (image optimization)
* - favicon.ico (browser icon)
* - public folder assets
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};
Notice the pattern: auth first, then redirects for logged-in users, then security headers on everything. Each concern is one short block. The matcher excludes static files so middleware doesn't waste cycles processing image requests.
Frequently Asked Questions
The middleware.ts (or middleware.js) file must live at the root of your project — the same level as your package.json. If you're using the src/ directory pattern, it goes inside src/. Next.js only recognizes one middleware file per project, and it must be in this exact location. If AI puts it inside app/ or pages/, it won't work.
By default, yes — middleware runs on every single request, including images, fonts, CSS files, and API routes. This is why the matcher config is critical. Without a matcher, your middleware processes requests for favicon.ico, every static asset, and every API call. Always configure a matcher to limit middleware to the routes that actually need it.
The Edge Runtime is a lightweight JavaScript environment that runs closer to your users (on CDN edge nodes) instead of on a central server. It starts in milliseconds, which is why middleware uses it — it needs to intercept requests fast without adding noticeable latency. The tradeoff is that Edge Runtime can't use many Node.js APIs like fs, native npm packages, or database drivers that need TCP connections.
Yes — auth protection is the most common use case. Check for a session token in cookies, and if it's missing, redirect to the login page. But middleware should only do lightweight token checks. Heavy auth logic — like database lookups or OAuth token refresh — should happen in your API routes or server components, not in middleware.
Middleware intercepts requests before they reach any page or API route. It's a gatekeeper — it can redirect, rewrite, or modify headers, but it's not meant to return full responses or do heavy processing. API routes (in app/api/) handle specific endpoints and can do database queries, send emails, process payments, etc. Think of middleware as the bouncer at the door and API routes as the staff inside the building.