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#
- Create middleware.ts: Add file at project root (next to
app/directory) - Export middleware function: Default export or named
middlewareexport - Configure matcher: Use
config.matcherto specify which paths to run on - Compose middlewares: Chain multiple middleware functions for complex logic
- Handle responses: Return
NextResponse.next()to continue or a response to short-circuit
Best Practices#
- Keep middleware fast - It runs on every matched request, avoid heavy computations
- Use matchers wisely - Only run middleware on necessary paths
- Compose for reusability - Break down logic into composable functions
- Add request IDs - Helpful for debugging and tracing
- Handle errors gracefully - Return proper error responses
- Consider edge limitations - Middleware runs at the edge, not all Node.js APIs available
- Log strategically - Don't over-log, use structured logging
Related Patterns#
- Route Handler - API route implementation
- Rate Limiting - Protect APIs from abuse
- Error Handling - Consistent error responses
- Authentication - User authentication setup