Next.js Middleware
Middleware is a powerful feature of Next.js that allows you to run code before a request is completed. This chapter will detail how to use middleware for authentication, redirects, internationalization, and other common scenarios.
Middleware Basics
Creating Middleware
Create a middleware.ts file in the project root directory:
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Middleware logic
console.log('Middleware executed:', request.nextUrl.pathname)
// Continue processing request
return NextResponse.next()
}
// Configure matching paths
export const config = {
matcher: [
/*
* Match all paths except:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}Basic Response Types
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 1. Continue processing request
if (pathname === '/continue') {
return NextResponse.next()
}
// 2. Redirect
if (pathname === '/old-page') {
return NextResponse.redirect(new URL('/new-page', request.url))
}
// 3. Rewrite URL
if (pathname === '/rewrite') {
return NextResponse.rewrite(new URL('/internal-page', request.url))
}
// 4. Return response
if (pathname === '/api/blocked') {
return new NextResponse('Access Denied', { status: 403 })
}
return NextResponse.next()
}Authentication Middleware
JWT Authentication
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!)
async function verifyToken(token: string) {
try {
const { payload } = await jwtVerify(token, JWT_SECRET)
return payload
} catch (error) {
return null
}
}
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Paths requiring authentication
const protectedPaths = ['/dashboard', '/profile', '/admin']
const isProtectedPath = protectedPaths.some(path =>
pathname.startsWith(path)
)
if (isProtectedPath) {
const token = request.cookies.get('token')?.value
if (!token) {
// Redirect to login page
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('redirect', pathname)
return NextResponse.redirect(loginUrl)
}
const user = await verifyToken(token)
if (!user) {
// Invalid token, clear cookie and redirect
const response = NextResponse.redirect(new URL('/login', request.url))
response.cookies.delete('token')
return response
}
// Add user info to request headers
const response = NextResponse.next()
response.headers.set('x-user-id', user.sub as string)
response.headers.set('x-user-role', user.role as string)
return response
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*', '/admin/:path*']
}Role-Based Access Control
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
interface User {
id: string
role: 'admin' | 'user' | 'moderator'
}
async function getUserFromToken(token: string): Promise<User | null> {
// Implement token verification logic
try {
const response = await fetch(`${process.env.API_URL}/auth/verify`, {
headers: { Authorization: `Bearer ${token}` }
})
if (response.ok) {
return await response.json()
}
} catch (error) {
console.error('Token verification failed:', error)
}
return null
}
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Define path permissions
const pathPermissions = {
'/admin': ['admin'],
'/moderator': ['admin', 'moderator'],
'/dashboard': ['admin', 'moderator', 'user'],
}
const requiredRoles = Object.entries(pathPermissions).find(([path]) =>
pathname.startsWith(path)
)?.[1]
if (requiredRoles) {
const token = request.cookies.get('auth-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
const user = await getUserFromToken(token)
if (!user || !requiredRoles.includes(user.role)) {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
// Add user info to request headers
const response = NextResponse.next()
response.headers.set('x-user-id', user.id)
response.headers.set('x-user-role', user.role)
return response
}
return NextResponse.next()
}Internationalization Middleware
Language Detection and Redirect
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const locales = ['en', 'zh', 'ja', 'ko']
const defaultLocale = 'en'
function getLocale(request: NextRequest): string {
// 1. Check language in URL path
const pathname = request.nextUrl.pathname
const pathnameLocale = locales.find(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (pathnameLocale) return pathnameLocale
// 2. Check cookie
const cookieLocale = request.cookies.get('locale')?.value
if (cookieLocale && locales.includes(cookieLocale)) {
return cookieLocale
}
// 3. Check Accept-Language header
const acceptLanguage = request.headers.get('accept-language')
if (acceptLanguage) {
const browserLocale = acceptLanguage
.split(',')[0]
.split('-')[0]
if (locales.includes(browserLocale)) {
return browserLocale
}
}
return defaultLocale
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Check if path already contains language prefix
const pathnameHasLocale = locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (!pathnameHasLocale) {
const locale = getLocale(request)
const newUrl = new URL(`/${locale}${pathname}`, request.url)
const response = NextResponse.redirect(newUrl)
response.cookies.set('locale', locale, { maxAge: 60 * 60 * 24 * 365 })
return response
}
return NextResponse.next()
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}Language Switcher
typescript
// components/LanguageSwitcher.tsx
'use client'
import { useRouter, usePathname } from 'next/navigation'
import { useState } from 'react'
const languages = {
en: 'English',
zh: '中文',
ja: '日本語',
ko: '한국어',
}
export default function LanguageSwitcher() {
const router = useRouter()
const pathname = usePathname()
const [currentLocale, setCurrentLocale] = useState('en')
const switchLanguage = (newLocale: string) => {
// Remove current language prefix
const pathWithoutLocale = pathname.replace(/^\/[a-z]{2}/, '')
// Add new language prefix
const newPath = `/${newLocale}${pathWithoutLocale}`
// Set cookie
document.cookie = `locale=${newLocale}; path=/; max-age=${60 * 60 * 24 * 365}`
setCurrentLocale(newLocale)
router.push(newPath)
}
return (
<select
value={currentLocale}
onChange={(e) => switchLanguage(e.target.value)}
className="border rounded px-2 py-1"
>
{Object.entries(languages).map(([code, name]) => (
<option key={code} value={code}>
{name}
</option>
))}
</select>
)
}Security Middleware
CSRF Protection
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import crypto from 'crypto'
function generateCSRFToken(): string {
return crypto.randomBytes(32).toString('hex')
}
function verifyCSRFToken(token: string, sessionToken: string): boolean {
return token === sessionToken
}
export function middleware(request: NextRequest) {
const { method, nextUrl } = request
// Only check CSRF for state-changing requests
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
const csrfToken = request.headers.get('x-csrf-token')
const sessionToken = request.cookies.get('csrf-token')?.value
if (!csrfToken || !sessionToken || !verifyCSRFToken(csrfToken, sessionToken)) {
return new NextResponse('CSRF token mismatch', { status: 403 })
}
}
// Generate CSRF token for GET requests
if (method === 'GET' && !request.cookies.get('csrf-token')) {
const token = generateCSRFToken()
const response = NextResponse.next()
response.cookies.set('csrf-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
})
return response
}
return NextResponse.next()
}Rate Limiting
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// Simple in-memory storage (use Redis in production)
const rateLimitMap = new Map<string, { count: number; resetTime: number }>()
function getRateLimitKey(request: NextRequest): string {
// Use IP address as limit key
const forwarded = request.headers.get('x-forwarded-for')
const ip = forwarded ? forwarded.split(',')[0] : request.ip || 'unknown'
return ip
}
function isRateLimited(key: string, limit: number, windowMs: number): boolean {
const now = Date.now()
const record = rateLimitMap.get(key)
if (!record || now > record.resetTime) {
// Reset or create new record
rateLimitMap.set(key, {
count: 1,
resetTime: now + windowMs
})
return false
}
if (record.count >= limit) {
return true
}
record.count++
return false
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Apply rate limiting to API routes
if (pathname.startsWith('/api/')) {
const key = getRateLimitKey(request)
const limit = 100 // 100 requests per minute
const windowMs = 60 * 1000 // 1 minute
if (isRateLimited(key, limit, windowMs)) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
'Retry-After': '60'
}
})
}
}
return NextResponse.next()
}Redirects and Rewrites
Dynamic Redirects
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// Redirect rules
const redirectRules = [
{ from: '/old-blog/:slug*', to: '/blog/:slug*' },
{ from: '/products/:id', to: '/shop/products/:id' },
{ from: '/user/:id', to: '/profile/:id' },
]
function applyRedirectRules(pathname: string): string | null {
for (const rule of redirectRules) {
const fromPattern = rule.from.replace(/:\w+\*/g, '(.*)').replace(/:\w+/g, '([^/]+)')
const regex = new RegExp(`^${fromPattern}$`)
const match = pathname.match(regex)
if (match) {
let to = rule.to
match.slice(1).forEach((param, index) => {
to = to.replace(/:\w+\*?/, param)
})
return to
}
}
return null
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Apply redirect rules
const redirectTo = applyRedirectRules(pathname)
if (redirectTo) {
return NextResponse.redirect(new URL(redirectTo, request.url))
}
return NextResponse.next()
}A/B Testing
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
function getVariant(request: NextRequest): 'A' | 'B' {
// Check existing cookie
const existingVariant = request.cookies.get('ab-test-variant')?.value
if (existingVariant === 'A' || existingVariant === 'B') {
return existingVariant
}
// Randomly assign variant
return Math.random() < 0.5 ? 'A' : 'B'
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Only A/B test the homepage
if (pathname === '/') {
const variant = getVariant(request)
let response: NextResponse
if (variant === 'B') {
// Rewrite to variant B page
response = NextResponse.rewrite(new URL('/home-variant-b', request.url))
} else {
response = NextResponse.next()
}
// Set variant cookie
response.cookies.set('ab-test-variant', variant, {
maxAge: 60 * 60 * 24 * 30, // 30 days
})
return response
}
return NextResponse.next()
}Logging and Monitoring
Request Logging
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
interface RequestLog {
timestamp: string
method: string
url: string
userAgent: string
ip: string
duration?: number
}
function logRequest(log: RequestLog) {
// Send to logging service
console.log(JSON.stringify(log))
// Or send to external service
// fetch('/api/logs', {
// method: 'POST',
// body: JSON.stringify(log)
// })
}
export function middleware(request: NextRequest) {
const startTime = Date.now()
const log: RequestLog = {
timestamp: new Date().toISOString(),
method: request.method,
url: request.url,
userAgent: request.headers.get('user-agent') || '',
ip: request.headers.get('x-forwarded-for') || request.ip || 'unknown',
}
const response = NextResponse.next()
// Add response time
response.headers.set('x-response-time', `${Date.now() - startTime}ms`)
// Log asynchronously
Promise.resolve().then(() => {
log.duration = Date.now() - startTime
logRequest(log)
})
return response
}Performance Monitoring
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const performanceMetrics = new Map<string, number[]>()
function recordMetric(path: string, duration: number) {
if (!performanceMetrics.has(path)) {
performanceMetrics.set(path, [])
}
const metrics = performanceMetrics.get(path)!
metrics.push(duration)
// Keep only the last 100 records
if (metrics.length > 100) {
metrics.shift()
}
}
function getAverageResponseTime(path: string): number {
const metrics = performanceMetrics.get(path)
if (!metrics || metrics.length === 0) return 0
const sum = metrics.reduce((a, b) => a + b, 0)
return sum / metrics.length
}
export function middleware(request: NextRequest) {
const startTime = Date.now()
const { pathname } = request.nextUrl
const response = NextResponse.next()
// Record response time
Promise.resolve().then(() => {
const duration = Date.now() - startTime
recordMetric(pathname, duration)
// Send warning if response time is too long
if (duration > 1000) {
console.warn(`Slow response: ${pathname} took ${duration}ms`)
}
})
return response
}Error Handling
Middleware Error Handling
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
try {
// Middleware logic
const { pathname } = request.nextUrl
// Operation that might throw an error
if (pathname === '/error-test') {
throw new Error('Test error')
}
return NextResponse.next()
} catch (error) {
console.error('Middleware error:', error)
// Return error response
return new NextResponse('Internal Server Error', {
status: 500,
headers: {
'Content-Type': 'text/plain',
},
})
}
}Testing Middleware
Unit Testing
typescript
// __tests__/middleware.test.ts
import { NextRequest } from 'next/server'
import { middleware } from '../middleware'
describe('Middleware', () => {
it('should redirect unauthenticated users', async () => {
const request = new NextRequest('http://localhost:3000/dashboard')
const response = await middleware(request)
expect(response.status).toBe(307) // Redirect status code
expect(response.headers.get('location')).toContain('/login')
})
it('should allow authenticated users', async () => {
const request = new NextRequest('http://localhost:3000/dashboard', {
headers: {
cookie: 'token=valid-jwt-token'
}
})
const response = await middleware(request)
expect(response.status).toBe(200)
})
})Best Practices
1. Performance Optimization
typescript
// ✅ Good practice - early return
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Skip static resources
if (pathname.startsWith('/_next/') || pathname.includes('.')) {
return NextResponse.next()
}
// Other logic...
}
// ❌ Avoid - complex synchronous operations
export function middleware(request: NextRequest) {
// Avoid complex computations
const result = heavyComputation()
return NextResponse.next()
}2. Error Handling
typescript
// ✅ Good practice
export async function middleware(request: NextRequest) {
try {
const user = await verifyToken(token)
return NextResponse.next()
} catch (error) {
console.error('Auth error:', error)
return NextResponse.redirect(new URL('/login', request.url))
}
}3. Matcher Configuration
typescript
// ✅ Good practice - precise matching
export const config = {
matcher: [
'/dashboard/:path*',
'/api/protected/:path*',
'/((?!_next/static|_next/image|favicon.ico).*)',
],
}
// ❌ Avoid - overly broad matching
export const config = {
matcher: '/:path*', // Matches all paths
}Summary
Key features of Next.js middleware:
- Request interception - Execute logic before request reaches the page
- Flexible responses - Redirect, rewrite, modify responses
- Performance optimization - Edge computing, reduced server load
- Security enhancement - Authentication, authorization, CSRF protection
- Monitoring and analytics - Request logging, performance metrics
Using middleware effectively can improve your application's security, performance, and user experience.