Next.js Middleware — Authentication and Routing
Advertisement
Next.js Middleware — Authentication and Routing
Middleware in Next.js runs on the edge before your routes execute, enabling authentication checks and request modification.
- Next.js Middleware — Authentication and Routing
- Creating Middleware
- Authentication Middleware
- Redirects Based on Path
- Request/Response Modification
- API Rate Limiting Middleware
- Locale-Based Routing
- Feature Flags Middleware
- Authentication with Session
- Real-World Example: Multi-Tenant Middleware
- FAQ
Creating Middleware
Create a middleware.ts file in your project root:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
console.log('Middleware running for:', request.nextUrl.pathname)
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
}
The matcher option controls which routes trigger middleware.
Authentication Middleware
Protect routes by checking authentication:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { jwtVerify } from 'jose'
const secret = new TextEncoder().encode(process.env.JWT_SECRET!)
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
await jwtVerify(token, secret)
return NextResponse.next()
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*', '/api/protected/:path*']
}
Redirects Based on Path
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
// Redirect /old-page to /new-page
if (pathname === '/old-page') {
return NextResponse.redirect(new URL('/new-page', request.url))
}
// Redirect based on locale
if (pathname === '/blog') {
const locale = request.headers.get('accept-language')?.split('-')[0] || 'en'
return NextResponse.redirect(new URL(`/${locale}/blog`, request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}
Request/Response Modification
Add headers or modify requests:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// Add custom headers
response.headers.set('X-Custom-Header', 'value')
response.headers.set('X-Request-ID', crypto.randomUUID())
// Add user info to request
const userId = request.cookies.get('user-id')?.value
if (userId) {
response.headers.set('X-User-ID', userId)
}
return response
}
API Rate Limiting Middleware
Implement rate limiting at the edge:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
const rateLimitStore = new Map()
export async function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api')) {
const clientIp = request.headers.get('x-forwarded-for') || 'unknown'
const now = Date.now()
const windowStart = now - 60000 // 1 minute window
const requests = rateLimitStore.get(clientIp) || []
const recentRequests = requests.filter(time => time > windowStart)
if (recentRequests.length >= 100) {
return new NextResponse(
JSON.stringify({ error: 'Rate limit exceeded' }),
{ status: 429 }
)
}
recentRequests.push(now)
rateLimitStore.set(clientIp, recentRequests)
}
return NextResponse.next()
}
Locale-Based Routing
Handle multiple languages:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
const locales = ['en', 'es', 'fr', 'de']
function getLocale(request: NextRequest): string {
const localeFromPath = locales.find(
locale => request.nextUrl.pathname.startsWith(`/${locale}`)
)
if (localeFromPath) return localeFromPath
const localeFromCookie = request.cookies.get('locale')?.value
if (localeFromCookie) return localeFromCookie
const localeFromHeader = request.headers
.get('accept-language')
?.split(',')[0]
.split('-')[0]
return localeFromHeader && locales.includes(localeFromHeader)
? localeFromHeader
: 'en'
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
// Skip public files
if (pathname.match(/^\/?(?:_next|api|public|favicon)/)) {
return NextResponse.next()
}
const hasLocale = locales.some(locale => pathname.startsWith(`/${locale}`))
if (!hasLocale) {
const locale = getLocale(request)
return NextResponse.redirect(
new URL(`/${locale}${pathname}`, request.url)
)
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!_next|api|public|favicon).*)']
}
Feature Flags Middleware
Control feature access via middleware:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
const enabledFeatures = {
darkMode: true,
betaFeatures: false,
analytics: true
}
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/beta')) {
if (!enabledFeatures.betaFeatures) {
return NextResponse.redirect(new URL('/404', request.url))
}
}
const response = NextResponse.next()
// Pass feature flags to app
Object.entries(enabledFeatures).forEach(([key, value]) => {
response.headers.set(`X-Feature-${key}`, String(value))
})
return response
}
Authentication with Session
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
// Unprotected routes
if (['/login', '/register', '/'].includes(pathname)) {
return NextResponse.next()
}
// Check session for protected routes
const session = await getSession(request)
if (!session) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', pathname)
return NextResponse.redirect(loginUrl)
}
// Admin routes require admin role
if (pathname.startsWith('/admin') && session.role !== 'admin') {
return NextResponse.redirect(new URL('/denied', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)'
]
}
Real-World Example: Multi-Tenant Middleware
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const hostname = request.headers.get('host')!
const pathname = request.nextUrl.pathname
// Extract subdomain
const subdomain = hostname.split('.')[0]
// Handle root domain
if (subdomain === 'localhost' || subdomain === 'example') {
return NextResponse.next()
}
// Route tenant requests to /tenant/[subdomain] without changing URL
const tenantPath = `/tenant/${subdomain}${pathname}`
return NextResponse.rewrite(
new URL(tenantPath, request.url)
)
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image).*)']
}
FAQ
Q: How does middleware differ from API routes? A: Middleware runs before all routes and has access to requests at the edge. API routes are specific endpoints. Middleware is ideal for cross-cutting concerns.
Q: Can middleware access the database? A: Yes, but edge middleware has limitations. Database calls work but may have latency. Keep middleware logic lightweight.
Q: What's the difference between matcher array and condition? A: matcher uses glob patterns to specify paths. You can also use conditional checks inside the middleware function for more complex logic.
Middleware enables powerful request-level control and security patterns in Next.js.
Advertisement