Middleware in Next.js
Middleware in Next.js is a function that runs before every request is processed. It lets you intercept requests and modify the response before it reaches the user.
How to Create Middleware
Middleware is defined in a middleware.ts file at the project root (next to app/ or pages/):
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname.startsWith('/admin')) {
const token = request.cookies.get('session')
if (!token) {
return NextResponse.redirect(new URL('/auth/login', request.url))
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/admin/:path*', '/settings/:path*']
}
Matcher
config.matcher defines which paths trigger the middleware:
export const config = {
matcher: [
'/admin/:path*',
'/settings/:path*',
'/((?!api|_next/static|_next/image|favicon.ico).*)'
]
}
Without a matcher, middleware runs for every request including static files. That is unnecessary overhead.
Common Scenarios
Authorization
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')
const isAuthPage = request.nextUrl.pathname.startsWith('/auth')
if (!token && !isAuthPage) {
return NextResponse.redirect(new URL('/auth/login', request.url))
}
if (token && isAuthPage) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return NextResponse.next()
}
Internationalization (i18n)
This is how localization works on Hack Frontend: middleware detects the user's language and redirects to the corresponding path.
const locales = ['ru', 'en']
const defaultLocale = 'ru'
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 locale = request.headers.get('accept-language')?.startsWith('en')
? 'en'
: defaultLocale
return NextResponse.redirect(
new URL(`/${locale}${pathname}`, request.url)
)
}
Adding Headers
export function middleware(request: NextRequest) {
const response = NextResponse.next()
response.headers.set('x-request-id', crypto.randomUUID())
response.headers.set(
'Content-Security-Policy',
"default-src 'self'"
)
return response
}
Edge Runtime
Middleware runs on Edge Runtime, not Node.js. This means:
- Fast startup (no cold start)
- Limited API (no file system, no native Node.js modules)
- Size limit (1 MB)
Limitations:
You cannot use Prisma, heavy npm packages or access the database directly in Middleware. For complex authorization logic it is better to verify the token in middleware and perform detailed permission checks in server components or Server Actions.
Chaining Actions
Middleware can perform multiple actions sequentially:
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// 1. Logging
console.log(`${request.method} ${request.nextUrl.pathname}`)
// 2. Adding a header
response.headers.set('x-pathname', request.nextUrl.pathname)
// 3. A/B testing
if (!request.cookies.get('ab-variant')) {
response.cookies.set('ab-variant', Math.random() > 0.5 ? 'A' : 'B')
}
return response
}
Useful Resources
- nextjs.org/docs — Middleware
- Middleware API Reference