Middleware Pattern

Implement request preprocessing, authentication, CORS, logging, and rate limiting using Next.js middleware for API routes.

Overview#

Middleware runs before requests are completed, allowing you to modify the response, redirect, rewrite, or add headers. It's ideal for authentication checks, logging, and request transformation across multiple routes.

When to use:

  • Authentication and authorization checks
  • Request logging and monitoring
  • CORS handling for API routes
  • Rate limiting implementation
  • Request/response header modification

Key features:

  • Runs at the edge for low latency
  • Executes before cached content
  • Can modify request and response
  • Path-based matching with matchers
  • Access to cookies and headers

Code Example#

Authentication Middleware#

1// middleware.ts 2import { NextResponse } from 'next/server' 3import type { NextRequest } from 'next/server' 4import { getToken } from 'next-auth/jwt' 5 6const protectedPaths = ['/api/users', '/api/teams', '/api/billing'] 7const publicPaths = ['/api/auth', '/api/health', '/api/webhooks'] 8 9export async function middleware(request: NextRequest) { 10 const path = request.nextUrl.pathname 11 12 // Skip public paths 13 if (publicPaths.some(p => path.startsWith(p))) { 14 return NextResponse.next() 15 } 16 17 // Check auth for protected paths 18 if (protectedPaths.some(p => path.startsWith(p))) { 19 const token = await getToken({ req: request }) 20 21 if (!token) { 22 return NextResponse.json( 23 { error: { message: 'Unauthorized', code: 'UNAUTHORIZED' } }, 24 { status: 401 } 25 ) 26 } 27 28 // Add user info to headers for route handlers 29 const requestHeaders = new Headers(request.headers) 30 requestHeaders.set('x-user-id', token.sub!) 31 requestHeaders.set('x-user-role', token.role as string) 32 33 return NextResponse.next({ 34 request: { headers: requestHeaders } 35 }) 36 } 37 38 return NextResponse.next() 39} 40 41export const config = { 42 matcher: '/api/:path*' 43}

CORS Middleware#

1// middleware.ts 2const allowedOrigins = [ 3 'https://app.example.com', 4 'https://admin.example.com' 5] 6 7function corsMiddleware(request: NextRequest) { 8 const origin = request.headers.get('origin') 9 const isAllowed = origin && allowedOrigins.includes(origin) 10 11 // Handle preflight 12 if (request.method === 'OPTIONS') { 13 return new NextResponse(null, { 14 status: 204, 15 headers: { 16 'Access-Control-Allow-Origin': isAllowed ? origin : '', 17 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 18 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 19 'Access-Control-Max-Age': '86400' 20 } 21 }) 22 } 23 24 const response = NextResponse.next() 25 26 if (isAllowed) { 27 response.headers.set('Access-Control-Allow-Origin', origin) 28 } 29 30 return response 31}

Request Logging Middleware#

1// middleware.ts 2import { NextResponse } from 'next/server' 3import type { NextRequest } from 'next/server' 4 5export async function middleware(request: NextRequest) { 6 const requestId = crypto.randomUUID() 7 const start = Date.now() 8 9 // Add request ID to headers 10 const requestHeaders = new Headers(request.headers) 11 requestHeaders.set('x-request-id', requestId) 12 13 const response = NextResponse.next({ 14 request: { headers: requestHeaders } 15 }) 16 17 // Log request 18 const duration = Date.now() - start 19 console.log(JSON.stringify({ 20 requestId, 21 method: request.method, 22 path: request.nextUrl.pathname, 23 duration, 24 timestamp: new Date().toISOString() 25 })) 26 27 response.headers.set('x-request-id', requestId) 28 return response 29}

Composed Middleware#

1// middleware.ts 2import { NextResponse } from 'next/server' 3import type { NextRequest, NextMiddleware } from 'next/server' 4 5type MiddlewareFunction = ( 6 request: NextRequest 7) => Promise<NextResponse | null> | NextResponse | null 8 9// Compose multiple middlewares 10function composeMiddleware(...middlewares: MiddlewareFunction[]) { 11 return async (request: NextRequest) => { 12 for (const middleware of middlewares) { 13 const result = await middleware(request) 14 // If middleware returns a response, stop chain 15 if (result) return result 16 } 17 return NextResponse.next() 18 } 19} 20 21// Individual middlewares 22async function authMiddleware(request: NextRequest) { 23 if (request.nextUrl.pathname.startsWith('/api/protected')) { 24 const token = await getToken({ req: request }) 25 if (!token) { 26 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 27 } 28 } 29 return null // Continue to next middleware 30} 31 32function loggingMiddleware(request: NextRequest) { 33 console.log(`${request.method} ${request.nextUrl.pathname}`) 34 return null 35} 36 37async function rateLimitMiddleware(request: NextRequest) { 38 const ip = request.ip ?? 'unknown' 39 const result = await checkRateLimit(ip) 40 if (!result.success) { 41 return NextResponse.json({ error: 'Rate limited' }, { status: 429 }) 42 } 43 return null 44} 45 46// Export composed middleware 47export const middleware = composeMiddleware( 48 loggingMiddleware, 49 rateLimitMiddleware, 50 authMiddleware 51) 52 53export const config = { 54 matcher: '/api/:path*' 55}

API Key Middleware#

1// lib/api-key.ts 2import { prisma } from '@/lib/db' 3import { headers } from 'next/headers' 4 5export async function validateApiKey() { 6 const headersList = headers() 7 const apiKey = headersList.get('x-api-key') 8 9 if (!apiKey) { 10 return null 11 } 12 13 const key = await prisma.apiKey.findUnique({ 14 where: { key: apiKey }, 15 include: { user: true } 16 }) 17 18 if (!key || key.revokedAt) { 19 return null 20 } 21 22 // Update last used 23 await prisma.apiKey.update({ 24 where: { id: key.id }, 25 data: { lastUsedAt: new Date() } 26 }) 27 28 return key.user 29} 30 31// Usage in route handler 32export async function GET(request: Request) { 33 const user = await validateApiKey() 34 35 if (!user) { 36 return Response.json( 37 { error: 'Invalid API key' }, 38 { status: 401 } 39 ) 40 } 41 42 // Continue with authenticated user... 43}

Request Validation Middleware#

1// lib/validate-request.ts 2import { NextRequest, NextResponse } from 'next/server' 3import { z } from 'zod' 4 5export function withValidation<T extends z.ZodSchema>( 6 schema: T, 7 handler: ( 8 request: NextRequest, 9 data: z.infer<T> 10 ) => Promise<NextResponse> 11) { 12 return async (request: NextRequest) => { 13 try { 14 const body = await request.json() 15 const data = schema.parse(body) 16 return handler(request, data) 17 } catch (error) { 18 if (error instanceof z.ZodError) { 19 return NextResponse.json( 20 { error: { message: 'Validation failed', details: error.errors } }, 21 { status: 400 } 22 ) 23 } 24 throw error 25 } 26 } 27} 28 29// Usage 30const CreateUserSchema = z.object({ 31 email: z.string().email(), 32 name: z.string().min(1) 33}) 34 35export const POST = withValidation(CreateUserSchema, async (request, data) => { 36 const user = await createUser(data) 37 return NextResponse.json({ data: user }, { status: 201 }) 38})

Usage Instructions#

  1. Create middleware.ts: Add file at project root (next to app/ directory)
  2. Export middleware function: Default export or named middleware export
  3. Configure matcher: Use config.matcher to specify which paths to run on
  4. Compose middlewares: Chain multiple middleware functions for complex logic
  5. Handle responses: Return NextResponse.next() to continue or a response to short-circuit

Best Practices#

  1. Keep middleware fast - It runs on every matched request, avoid heavy computations
  2. Use matchers wisely - Only run middleware on necessary paths
  3. Compose for reusability - Break down logic into composable functions
  4. Add request IDs - Helpful for debugging and tracing
  5. Handle errors gracefully - Return proper error responses
  6. Consider edge limitations - Middleware runs at the edge, not all Node.js APIs available
  7. Log strategically - Don't over-log, use structured logging