Suggest an editImprove this articleRefine the answer for “middleware in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Next.js middleware** is a function in `middleware.ts` that runs on Edge Runtime before requests reach your routes. It handles auth, i18n, and header injection without touching page components. ```ts export function middleware(request: NextRequest) { if (!request.cookies.get('session')) return NextResponse.redirect(new URL('/login', request.url)) return NextResponse.next() } export const config = { matcher: '/app/:path*' } ``` **Key point:** always add `config.matcher`, otherwise middleware runs on every static file request too.Shown above the full answer for quick recall.Answer (EN)Image**Next.js middleware** is a TypeScript function in `middleware.ts` that runs on the Edge Network before your routes, letting you intercept, inspect, and redirect requests without touching a single page component. ## Theory ### TL;DR - Think of it as an airport security checkpoint: every request passes through once, and you decide whether it gets redirected, rewritten, or passed through. - Runs on Edge Runtime (V8 isolates, not Node.js), so no cold starts and no access to `fs` or Prisma. - Without `config.matcher`, it fires on every request including static files. Always add a matcher. - Best fit: auth guards, i18n redirects, CORS headers. Bad fit: direct database queries. - One `middleware.ts` per project. No native chaining like Express. ### Quick example ```ts // middleware.ts at project root import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export function middleware(request: NextRequest) { if (request.nextUrl.pathname.startsWith('/dashboard')) { const token = request.cookies.get('auth')?.value if (!token) return NextResponse.redirect(new URL('/login', request.url)) } return NextResponse.next() } // Fires only on /dashboard and sub-paths; skips /api, /_next/static export const config = { matcher: '/dashboard/:path*' } ``` Any unauthenticated request to `/dashboard/settings` hits the middleware, gets no cookie, and lands on `/login`. Authenticated requests pass through untouched. ### Key difference from Express Next.js middleware runs on the Edge Network before the request reaches your serverless functions or pages. Express middleware runs per-instance in Node.js after the server is already up. The practical difference: Next.js middleware is globally distributed and stateless, while Express middleware can hold state and use any Node.js module. That is a real tradeoff you need to know before an interview. ### When to use - Auth checks on protected routes (`/app/*`, `/admin/*`) - middleware - Locale detection and URL rewriting - middleware - Adding security headers (CSP, CORS) - middleware - A/B test bucket assignment via cookies - middleware - Rate limiting `/api/*` with an external store like Upstash - middleware - Complex permission logic that needs the database - server component or route handler - Paths with more than 50% of your total traffic - reconsider, Edge has request limits ### How it works internally Next.js bundles `middleware.ts` into a WebAssembly-like module for Edge Runtime (V8 isolates). On each request: the matcher regex checks the path in O(1); if it matches, the runtime invokes your function with a `NextRequest` object built on the Web Fetch API; your function returns a `NextResponse` that signals redirect, rewrite, or pass-through to the router. No Node.js process is involved at any point. `NextRequest` gives you `cookies`, `headers`, `nextUrl`, and `geo`. `NextResponse` lets you call `redirect()`, `rewrite()`, or `next()` with optional header mutations. You cannot mutate the original request directly because it is immutable. ### Common mistakes **1. Using `export default` instead of a named export.** ```ts // Wrong - produces no middleware at runtime, no build error either export default function middleware(request) { ... } // Fix export function middleware(request: NextRequest) { ... } ``` Next.js requires the named export. The default export compiles fine and silently does nothing. **2. Skipping `config.matcher`.** Without a matcher, middleware runs on every request including `/_next/static`, `/_next/image`, and `favicon.ico`. On Vercel's free tier this burns your middleware quota fast. Add a matcher. **3. `await fetch` without a timeout.** Edge Runtime has a 30-second wall clock limit. A hanging external fetch stalls the entire request. Fix: ```ts const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 5000) const res = await fetch('https://auth-service.example.com/verify', { signal: controller.signal }) clearTimeout(timeout) ``` **4. Trying to use Node.js modules.** `fs`, `crypto` (Node flavor), Prisma, `bcrypt` - none of these work in Edge Runtime. The build fails with a module resolution error. Use Edge-compatible alternatives: `@vercel/kv`, `@upstash/redis`, Web Crypto API. **5. Mutating the request object directly.** `NextRequest` is immutable. Setting headers on it throws a `TypeError`. To pass data downstream, use response headers, cookies, or rewrite the URL with search params. ### Real-world usage - **Vercel Dashboard**: auth middleware redirects unauthenticated users from `/team/*` - **Clerk / NextAuth.js**: session cookie checks on all protected routes - **next-intl**: rewrites `/` to `/en` or `/de` based on the `Accept-Language` header - **Stripe integrations**: rate limiting `/api/stripe/*` via Upstash sliding window - **Create T3 App**: middleware for admin auth and CSRF header injection ### Follow-up questions **Q:** How does middleware differ from App Router route handlers? **A:** Middleware runs before routing on the Edge with no React context. Route handlers run per-route on the server with full Node.js access. For auth: middleware is the first-pass check, route handlers handle the business logic. **Q:** Can middleware access a database directly? **A:** No. Edge Runtime is stateless with no persistent connections. For state, use external services: Upstash Redis, PlanetScale serverless driver, or Vercel KV. Middleware checks a token; server components do the permission query. **Q:** How do you handle multiple middlewares? **A:** There is one `middleware.ts` per Next.js project. Compose logic with sequential `if` blocks or use a router library like `hono` for mini-routes inside the single file. There is no true chaining like Express `app.use()`. **Q:** What is the matcher syntax for skipping static files? **A:** `'/((?!api|_next/static|_next/image|favicon.ico).*)'`. The negative lookahead skips those prefixes. Without it, middleware runs on every static asset request. **Q:** What happens when middleware needs to both redirect and set a cookie? **A:** Only the first returned `NextResponse` wins. Once you `return NextResponse.redirect(...)`, the function exits. If you need a cookie on a redirect, create the redirect response, call `.cookies.set()` on it, then return it. **Q:** What changed in Next.js 15 for middleware? **A:** Turbopack support landed and async matcher config became experimental. The core API (`NextRequest`, `NextResponse`, `config.matcher`) stayed the same. ## Examples ### Auth guard with redirect URL preservation ```ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export function middleware(request: NextRequest) { const { pathname } = request.nextUrl const token = request.cookies.get('session')?.value if (pathname.startsWith('/app') && !token) { // Preserve the original URL so login can redirect back const loginUrl = new URL('/login', request.url) loginUrl.searchParams.set('from', pathname) return NextResponse.redirect(loginUrl) } return NextResponse.next() } export const config = { matcher: '/app/:path*' } // /app/settings with no cookie → /login?from=/app/settings // /app/settings with valid cookie → proceeds normally ``` After login, your login page reads `?from` and redirects the user back to where they were. A small thing that makes auth feel polished in production. ### Locale detection and URL rewriting ```ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' const locales = ['en', 'de', 'fr'] export function middleware(request: NextRequest) { const { pathname } = request.nextUrl const hasLocale = locales.some( locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}` ) if (hasLocale) return NextResponse.next() const acceptLanguage = request.headers.get('accept-language') ?? '' const preferred = acceptLanguage.split(',')[0].slice(0, 2) const locale = locales.includes(preferred) ? preferred : 'en' // Rewrite preserves the URL in the browser; redirect would change it return NextResponse.rewrite(new URL(`/${locale}${pathname}`, request.url)) } export const config = { matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)' } // GET / with Accept-Language: de,en → renders /de/ without changing browser URL ``` The difference between `redirect` and `rewrite` matters here. `rewrite` changes what renders; `redirect` changes the URL the user sees. For i18n, `rewrite` is almost always the right call. ### Rate limiting API routes with Upstash Redis ```ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' import { Ratelimit } from '@upstash/ratelimit' import { Redis } from '@upstash/redis' const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 requests per minute per IP }) export async function middleware(request: NextRequest) { if (request.nextUrl.pathname.startsWith('/api')) { const ip = request.headers.get('x-forwarded-for') ?? 'anonymous' const { success } = await ratelimit.limit(ip) if (!success) { return new NextResponse('Too Many Requests', { status: 429, headers: { 'Retry-After': '60' } }) } const response = NextResponse.next() response.headers.set('Access-Control-Allow-Origin', '*') response.headers.set('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE') return response } return NextResponse.next() } export const config = { matcher: '/api/:path*' } // Each IP gets 10 requests per minute on /api routes // Over the limit → 429 with Retry-After header ``` One thing to watch: if `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` are missing from your environment, `Redis.fromEnv()` throws at cold start and takes down your entire middleware layer. Add those env vars before deploying.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.