1. Introduction đ
Middleware lets you run code before a request completes, giving you a single place to handle redirects, rewrites, authentication checks, and header manipulation â all before a page or Route Handler ever runs. This tutorial covers everything from the basic file setup to advanced patterns like RBAC and internationalization.
Information
2. What is Middleware? đ¤
Middleware is a single function, exported from a middleware.ts file at the root of your project, that intercepts requests before they reach a route. It can inspect, modify, redirect, or short-circuit the request entirely.
- Runs before cache and route matching.
- Ideal for cross-cutting concerns: auth, logging, i18n, feature flags.
- Built on the same Request/Response Web APIs as Route Handlers.
3. How Middleware Works âī¸
When a request comes in, Next.js checks whether it matches your middleware.ts config. If it does, the exported middleware() function runs first, and its returned NextResponse decides what happens next: continue, redirect, rewrite, or return a direct response.
4. Middleware File Structure đ
Middleware MUST live in a single middleware.ts (or .js) file at the root of your project â either directly inside the project root or inside src if you use that convention.
Warning
5. Request Lifecycle đ
6. Request Inspection đ
The middleware function receives a NextRequest object, giving you access to the URL, headers, cookies, and geo/IP information (when deployed on supporting infrastructure).
middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
console.log(request.nextUrl.pathname);
console.log(request.method);
return NextResponse.next();
}7. Response Modification âī¸
Middleware can modify the outgoing response by cloning headers, injecting new ones, or attaching cookies before passing the request along with NextResponse.next().
middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request: Request) {
const response = NextResponse.next();
response.headers.set('X-Powered-By', 'My-App');
return response;
}8. Redirects âĒī¸
Use NextResponse.redirect() to send the client to a different URL, optionally with a specific status code such as 307 or 308.
middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === '/old-page') {
return NextResponse.redirect(new URL('/new-page', request.url));
}
return NextResponse.next();
}9. Rewrites đ
A rewrite serves content from a different path while keeping the URL in the browser unchanged â useful for A/B testing, proxying, or masking internal routes.
middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === '/dashboard') {
return NextResponse.rewrite(new URL('/dashboard/v2', request.url));
}
return NextResponse.next();
}Tip
10. Authentication đ
Middleware is a natural place to check whether a request carries a valid session before any page code runs, avoiding wasted rendering work for unauthenticated users.
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) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}11. Authorization đĄī¸
Once a user is authenticated, middleware can also verify what they're allowed to access â for example, decoding a JWT to check a permission claim.
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;
const payload = token ? decodeJwt(token) : null;
if (!payload?.permissions?.includes('read:reports')) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
return NextResponse.next();
}12. Route Protection đ§
Combine a matcher with an authentication check to guard entire route groups, like /dashboard or /admin, without repeating logic in every page.
middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const isLoggedIn = Boolean(request.cookies.get('session'));
if (!isLoggedIn) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};13. Role-Based Access Control
RBAC restricts specific paths to specific user roles. Middleware can read a role claim from the session and branch accordingly.
middleware.ts
export function middleware(request: NextRequest) {
const role = getUserRole(request);
if (request.nextUrl.pathname.startsWith('/admin') && role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
return NextResponse.next();
}Best Practice
14. Cookies đĒ
request.cookies and response.cookies provide a simple API for reading and setting cookies directly from middleware.
middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
const visited = request.cookies.get('visited');
if (!visited) {
response.cookies.set('visited', 'true', { maxAge: 60 * 60 * 24 });
}
return response;
}15. Headers đˇī¸
You can read incoming headers via request.headers and set outgoing ones on the NextResponse you return.
middleware.ts
export function middleware(request: NextRequest) {
const country = request.headers.get('x-vercel-ip-country');
const response = NextResponse.next();
response.headers.set('X-User-Country', country ?? 'unknown');
return response;
}16. URL Matching đ¯
Inside the middleware function itself, you can branch on request.nextUrl.pathname to apply different logic to different paths.
middleware.ts
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname.startsWith('/api')) {
return handleApiRequest(request);
}
if (pathname.startsWith('/blog')) {
return handleBlogRequest(request);
}
return NextResponse.next();
}17. Matcher Configuration đ§
The exported config.matcher tells Next.js which paths should invoke middleware at all â this is more efficient than running the function on every request and filtering inside it.
middleware.ts
export const config = {
matcher: [
'/dashboard/:path*',
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};| Pattern | Meaning |
|---|---|
| /dashboard/:path* | Matches /dashboard and all nested paths |
| /((?!_next).*) | Matches everything except internal Next.js assets |
18. Conditional Middleware đ
Beyond the static matcher, you can add runtime conditions inside the function itself â for example, skipping logic for certain user agents or feature flags.
middleware.ts
export function middleware(request: NextRequest) {
const featureEnabled = request.cookies.get('beta')?.value === 'true';
if (!featureEnabled) {
return NextResponse.next();
}
return NextResponse.rewrite(new URL('/beta-home', request.url));
}19. Internationalization (i18n) đ
Middleware is a common place to detect a visitor's preferred locale â from the Accept-Language header or a cookie â and rewrite or redirect to the localized path.
middleware.ts
const locales = ['en', 'fr', 'de'];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const hasLocale = locales.some((locale) => pathname.startsWith(`/${locale}`));
if (!hasLocale) {
const preferred = request.headers.get('accept-language')?.split(',')[0].slice(0, 2) ?? 'en';
const locale = locales.includes(preferred) ? preferred : 'en';
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
}
return NextResponse.next();
}20. Localization
Once a locale is detected, you can also persist the user's choice in a cookie so future visits skip the detection step entirely.
middleware.ts
export function middleware(request: NextRequest) {
const response = NextResponse.next();
const locale = request.nextUrl.pathname.split('/')[1];
if (locale) {
response.cookies.set('NEXT_LOCALE', locale);
}
return response;
}21. Rate Limiting đĻ
Middleware can enforce simple rate limits by tracking request counts per IP address, typically backed by an external store like Redis or an edge-compatible KV store.
middleware.ts
export async function middleware(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
const count = await incrementRequestCount(ip);
if (count > 100) {
return NextResponse.json({ error: 'Too many requests' }, { status: 429 });
}
return NextResponse.next();
}Warning
22. Bot Detection đ¤
Inspecting the User-Agent header lets you flag or block known crawlers and bots before they reach expensive routes.
middleware.ts
export function middleware(request: NextRequest) {
const userAgent = request.headers.get('user-agent') ?? '';
const isBot = /bot|crawler|spider/i.test(userAgent);
if (isBot && request.nextUrl.pathname.startsWith('/checkout')) {
return NextResponse.json({ error: 'Not allowed' }, { status: 403 });
}
return NextResponse.next();
}23. Security Headers đ
Middleware is a convenient single place to attach security-related headers â like Content-Security-Policy or X-Frame-Options â to every response.
middleware.ts
export function middleware(request: NextRequest) {
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;
}24. CORS đ
Middleware can centrally enforce CORS rules for API-like routes, including handling OPTIONS preflight requests.
middleware.ts
export function middleware(request: NextRequest) {
if (request.method === 'OPTIONS') {
return new NextResponse(null, {
headers: {
'Access-Control-Allow-Origin': 'https://example.com',
'Access-Control-Allow-Methods': 'GET, POST',
},
});
}
const response = NextResponse.next();
response.headers.set('Access-Control-Allow-Origin', 'https://example.com');
return response;
}
export const config = { matcher: '/api/:path*' };25. Edge Runtime âĄ
Middleware always runs on the Edge Runtime â a lightweight V8 environment deployed close to the user. This keeps middleware fast, but it means full Node.js APIs are unavailable.
- No direct file system access (fs is unavailable).
- Limited subset of Node.js built-in modules.
- Favor fetch-based calls to external services over native drivers.
26. Performance Considerations đī¸
- Keep middleware logic lightweight â it runs on every matched request.
- Scope execution tightly with config.matcher instead of checking paths manually inside the function.
- Avoid expensive synchronous computation; prefer async calls to fast, edge-compatible data stores.
- Cache results (e.g. decoded JWT claims) where possible to avoid repeated work per request.
27. Debugging Middleware đ
Since middleware runs on the edge, console.log output appears in your terminal during local development and in your hosting provider's edge logs in production.
middleware.ts
export function middleware(request: NextRequest) {
console.log('[middleware]', request.method, request.nextUrl.pathname);
return NextResponse.next();
}Tip
28. Middleware Limitations đĢ
- Only one middleware.ts file is allowed per project.
- No access to the full Node.js runtime (no fs, no native modules).
- Cannot directly read a request body for use in routing decisions without extra handling.
- Adds latency to every matched request, so scope and complexity matter.
29. Best Practices â
- Scope middleware with a precise matcher â never run it project-wide unless truly necessary.
- Keep authentication checks fast; verify tokens rather than performing full database lookups when possible.
- Centralize security headers here instead of duplicating them in every route.
- Return early with NextResponse.next() for requests that don't need special handling.
- Log meaningful context (path, method, decision) for easier debugging in production.
30. Common Mistakes đĢ
31. Frequently Asked Questions â
Middleware can call request.json() or similar methods, but doing so consumes the body stream â take care if the route handler downstream also needs to read it.
No â Next.js supports exactly one middleware.ts file at the project root. Compose all logic within that single function.
Yes, if the path matches your config.matcher â middleware runs before Next.js determines whether a page is static or dynamic.