// LOADING
// LOADING
// LOADING_ARTICLE
Next.js middleware runs before a request reaches your pages or API routes. It executes at the Edge, meaning it runs in a lightweight runtime close to the user — not in a full Node.js environment. That constraint matters a lot for what you can and cannot do.
Middleware is the right place for logic that must run on every (or nearly every) request and doesn't need the full Node.js runtime:
It is not the right place for database queries, heavy computation, or anything that requires Node-only APIs like fs. The Edge runtime does not have those.
Drop a middleware.ts at the root of your project (same level as app/):
ts// middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { const token = request.cookies.get('session')?.value; if (!token && request.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); } export const config = { matcher: ['/dashboard/:path*', '/admin/:path*'], };
The matcher config is critical. Without it, middleware runs on every single request — including _next/static assets, which wastes cycles and can interfere with fast asset delivery. Always scope matchers tightly.
Verifying a JWT at the Edge is possible because libraries like jose are Edge-compatible. Avoid any library that requires Node's crypto module directly.
tsimport { jwtVerify } from 'jose'; const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!); export async function middleware(request: NextRequest) { const token = request.cookies.get('session')?.value; if (!token) { return NextResponse.redirect(new URL('/login', request.url)); } try { await jwtVerify(token, SECRET); return NextResponse.next(); } catch { // Token invalid or expired const response = NextResponse.redirect(new URL('/login', request.url)); response.cookies.delete('session'); return response; } }
This is a clean auth gate: no database call, just cryptographic verification. For details on how the full token flow works — including refresh token rotation — see the notes on API authentication with refresh tokens.
Middleware can also rewrite URLs without changing what the user sees in the browser:
ts// Serve /us/pricing from /pricing for US visitors export function middleware(request: NextRequest) { const country = request.geo?.country ?? 'US'; if (request.nextUrl.pathname === '/pricing' && country === 'US') { return NextResponse.rewrite(new URL('/us/pricing', request.url)); } return NextResponse.next(); }
Rewrites are invisible to the user — the URL stays the same but a different page is rendered. Redirects change the URL with a 307 (temporary) or 308 (permanent). Use rewrites for personalization, redirects for auth and canonical URL enforcement.
Middleware can inject headers that your pages and API routes then read, or add security headers to every response:
tsexport function middleware(request: NextRequest) { const response = NextResponse.next(); // Pass user identity downstream without a DB call response.headers.set('x-user-id', getUserIdFromToken(request)); // Security headers response.headers.set('X-Frame-Options', 'DENY'); response.headers.set('X-Content-Type-Options', 'nosniff'); return response; }
This pattern lets Server Components and Route Handlers read x-user-id from request headers without re-verifying the token. It fits naturally into the layered approach described in how to design APIs for Next.js applications.
Importing Node-only code. If you import anything that touches fs, child_process, or Node's crypto, the Edge runtime will throw at build time. Check library documentation for Edge compatibility before using it in middleware.
Forgetting the matcher. Running middleware on static assets silently degrades performance. At minimum, exclude _next/static and _next/image:
tsexport const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico).*)', ], };
Storing sensitive data in cookies without encryption. Middleware can read cookies, but if you store user roles or permissions in a plain cookie, anyone can tamper with it. Either verify a signed token or store only an opaque session ID and look up roles server-side.
Doing too much. Middleware adds latency to every matched request. If logic is only needed for one route, a Server Component or Route Handler is a better place for it.
If your auth check requires a database lookup (e.g., checking if a session is revoked), middleware at the Edge is the wrong layer — you'd be making a network call on every request. Instead, do a lightweight token check in middleware and a full session check in your server-side data layer. The distinction between route handlers vs server actions is also relevant here: choose the right layer for the right job.
For additional protection on your API endpoints — rate limiting, for instance — middleware is one option but dedicated logic at the route level is often more precise. See rate limiting your Next.js APIs for patterns that work well alongside middleware.
Next.js middleware is a powerful primitive when used for what it's actually good at: fast, Edge-runtime-compatible gatekeeping and request transformation. Keep your matchers tight, use Edge-compatible JWT libraries for token verification, and push anything that needs the full Node.js runtime down to Route Handlers or Server Components. Check out the projects section to see how these patterns appear in production Next.js applications.