Next.js Middleware is a powerful feature that allows you to execute code before a request completes. This is very useful for scenarios like authentication, redirects, A/B testing, etc.
What is Middleware?
Middleware is a function that runs in your Next.js application and intercepts incoming requests before they complete. Middleware can:
- Rewrite paths: Rewrite one path to another
- Redirect: Redirect users to different URLs
- Modify request/response: Add or modify request headers, response headers
- Authentication: Check if user is logged in
- Geo-location routing: Redirect based on user location
Basic Usage
Create Middleware File
Create middleware.js or middleware.ts file in project root:
javascript// middleware.js import { NextResponse } from 'next/server'; export function middleware(request) { return NextResponse.next(); } export const config = { matcher: '/about/:path*', };
Middleware Configuration
javascript// middleware.js export const config = { matcher: [ // Match all paths '/((?!api|_next/static|_next/image|favicon.ico).*)', // Match specific paths '/dashboard/:path*', // Use regex '/((?!api|_next/static|_next/image|favicon.ico).*)', // Match multiple paths ['/about/:path*', '/contact/:path*'], ], };
Middleware API
NextResponse Methods
javascriptimport { NextResponse } from 'next/server'; export function middleware(request) { // Continue to next middleware or page return NextResponse.next(); // Redirect return NextResponse.redirect(new URL('/login', request.url)); // Rewrite path return NextResponse.rewrite(new URL('/about', request.url)); // Return custom response return NextResponse.json({ message: 'Hello' }); }
Modify Request and Response
javascriptimport { NextResponse } from 'next/server'; export function middleware(request) { const response = NextResponse.next(); // Add response headers response.headers.set('x-custom-header', 'custom-value'); // Set cookie response.cookies.set('theme', 'dark', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', }); return response; }
Real-world Use Cases
1. Authentication Protection
javascript// middleware.js import { NextResponse } from 'next/server'; export function middleware(request) { const { pathname } = request.nextUrl; // Public paths const publicPaths = ['/login', '/register', '/api/auth']; const isPublicPath = publicPaths.some(path => pathname.startsWith(path) ); if (isPublicPath) { return NextResponse.next(); } // Check auth token const token = request.cookies.get('auth-token')?.value; if (!token) { return NextResponse.redirect(new URL('/login', request.url)); } // Verify token try { const user = verifyToken(token); // 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; } catch (error) { return NextResponse.redirect(new URL('/login', request.url)); } } export const config = { matcher: [ '/((?!api|_next/static|_next/image|favicon.ico).*)', ], };
2. Internationalization Routing
javascript// middleware.js import { NextResponse } from 'next/server'; const locales = ['en', 'zh', 'es']; const defaultLocale = 'en'; export function middleware(request) { const { pathname } = request.nextUrl; // Check if path contains language code const pathnameIsMissingLocale = locales.every( locale => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}` ); if (pathnameIsMissingLocale) { // Check Accept-Language header const acceptLanguage = request.headers.get('accept-language') || ''; const preferredLocale = locales.find(locale => acceptLanguage.includes(locale) ) || defaultLocale; return NextResponse.redirect( new URL(`/${preferredLocale}${pathname}`, request.url) ); } return NextResponse.next(); } export const config = { matcher: [ '/((?!api|_next/static|_next/image|favicon.ico).*)', ], };
3. A/B Testing
javascript// middleware.js import { NextResponse } from 'next/server'; export function middleware(request) { const { pathname } = request.nextUrl; // Only A/B test specific pages if (pathname === '/landing') { // Get or create user ID const userId = request.cookies.get('user-id')?.value || generateUserId(); // Decide which version to show based on ID const variant = hash(userId) % 2 === 0 ? 'a' : 'b'; const response = NextResponse.rewrite( new URL(`/landing-${variant}`, request.url) ); // Set user ID cookie if (!request.cookies.get('user-id')) { response.cookies.set('user-id', userId); } // Add A/B test header response.headers.set('x-ab-variant', variant); return response; } return NextResponse.next(); } function generateUserId() { return Math.random().toString(36).substring(2, 15); } function hash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash); }
4. Geo-location Routing
javascript// middleware.js import { NextResponse } from 'next/server'; export function middleware(request) { const { pathname } = request.nextUrl; // Only geo-route homepage if (pathname === '/') { const country = request.geo?.country || 'US'; // Redirect to different versions based on country const countryRoutes = { 'US': '/us', 'CN': '/cn', 'JP': '/jp', 'DE': '/de', }; const targetPath = countryRoutes[country] || '/us'; return NextResponse.redirect( new URL(targetPath, request.url) ); } return NextResponse.next(); }
5. Maintenance Mode
javascript// middleware.js import { NextResponse } from 'next/server'; export function middleware(request) { const { pathname } = request.nextUrl; // Check if in maintenance mode const isMaintenanceMode = process.env.MAINTENANCE_MODE === 'true'; if (isMaintenanceMode) { // Allow admin access const isAdmin = request.cookies.get('admin-token')?.value === process.env.ADMIN_TOKEN; if (isAdmin) { return NextResponse.next(); } // Redirect to maintenance page return NextResponse.rewrite( new URL('/maintenance', request.url) ); } return NextResponse.next(); } export const config = { matcher: [ '/((?!api|_next/static|_next/image|favicon.ico|maintenance).*)', ], };
6. Rate Limiting
javascript// middleware.js import { NextResponse } from 'next/server'; const rateLimit = new Map(); export function middleware(request) { const { pathname } = request.nextUrl; // Only rate limit API routes if (!pathname.startsWith('/api/')) { return NextResponse.next(); } const ip = request.ip || 'unknown'; const now = Date.now(); const windowMs = 60 * 1000; // 1 minute const maxRequests = 100; // Get or create rate limit record const record = rateLimit.get(ip) || { count: 0, resetTime: now + windowMs }; // Reset expired records if (now > record.resetTime) { record.count = 0; record.resetTime = now + windowMs; } // Increment request count record.count++; rateLimit.set(ip, record); // Check if limit exceeded if (record.count > maxRequests) { return NextResponse.json( { error: 'Too many requests' }, { status: 429, headers: { 'Retry-After': Math.ceil((record.resetTime - now) / 1000).toString(), }, } ); } return NextResponse.next(); }
7. CORS Configuration
javascript// middleware.js import { NextResponse } from 'next/server'; export function middleware(request) { const { pathname } = request.nextUrl; // Only apply CORS to API routes if (!pathname.startsWith('/api/')) { return NextResponse.next(); } const response = NextResponse.next(); // Set CORS headers response.headers.set('Access-Control-Allow-Credentials', 'true'); response.headers.set('Access-Control-Allow-Origin', '*'); response.headers.set('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT'); response.headers.set( 'Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version' ); return response; }
8. Logging
javascript// middleware.js import { NextResponse } from 'next/server'; export function middleware(request) { const start = Date.now(); const response = NextResponse.next(); // Log request info const logData = { method: request.method, url: request.url, userAgent: request.headers.get('user-agent'), ip: request.ip, timestamp: new Date().toISOString(), }; console.log('Request:', JSON.stringify(logData)); // Log response time response.headers.set('x-response-time', `${Date.now() - start}ms`); return response; }
Advanced Usage
1. Conditional Middleware
javascript// middleware.js import { NextResponse } from 'next/server'; export function middleware(request) { const { pathname } = request.nextUrl; // Apply different logic based on path if (pathname.startsWith('/api/')) { return apiMiddleware(request); } else if (pathname.startsWith('/dashboard/')) { return dashboardMiddleware(request); } else if (pathname.startsWith('/admin/')) { return adminMiddleware(request); } return NextResponse.next(); } function apiMiddleware(request) { // API specific logic return NextResponse.next(); } function dashboardMiddleware(request) { // Dashboard specific logic return NextResponse.next(); } function adminMiddleware(request) { // Admin specific logic return NextResponse.next(); }
2. Chained Middleware
javascript// middleware.js import { NextResponse } from 'next/server'; export function middleware(request) { const middlewares = [ authMiddleware, rateLimitMiddleware, loggingMiddleware, ]; for (const mw of middlewares) { const result = mw(request); if (result) { return result; } } return NextResponse.next(); } function authMiddleware(request) { const token = request.cookies.get('auth-token')?.value; if (!token) { return NextResponse.redirect(new URL('/login', request.url)); } return null; } function rateLimitMiddleware(request) { // Rate limiting logic return null; } function loggingMiddleware(request) { // Logging logic return null; }
3. Dynamic Rewrites
javascript// middleware.js import { NextResponse } from 'next/server'; export function middleware(request) { const { pathname } = request.nextUrl; // Dynamic route rewrite if (pathname.startsWith('/blog/')) { const slug = pathname.split('/')[2]; return NextResponse.rewrite( new URL(`/api/blog?slug=${slug}`, request.url) ); } // User profile rewrite if (pathname.startsWith('/u/')) { const username = pathname.split('/')[2]; return NextResponse.rewrite( new URL(`/profile?username=${username}`, request.url) ); } return NextResponse.next(); }
Best Practices
1. Avoid Heavy Operations in Middleware
javascript// ❌ Bad: Database query in middleware export function middleware(request) { const user = await db.user.findUnique({ where: { id: request.cookies.get('user-id')?.value } }); if (!user) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); } // ✅ Good: Only verify token export function middleware(request) { const token = request.cookies.get('auth-token')?.value; if (!token || !verifyToken(token)) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); }
2. Use Matcher for Precise Matching
javascript// ❌ Bad: Match all paths export const config = { matcher: '/(.*)', }; // ✅ Good: Exclude unnecessary paths export const config = { matcher: [ '/((?!api|_next/static|_next/image|favicon.ico).*)', ], };
3. Handle Edge Cases
javascriptexport function middleware(request) { // Handle OPTIONS requests if (request.method === 'OPTIONS') { return new NextResponse(null, { status: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, }); } return NextResponse.next(); }
4. Use Environment Variables
javascriptexport function middleware(request) { // Apply different logic based on environment if (process.env.NODE_ENV === 'production') { // Production specific logic return productionMiddleware(request); } else { // Development specific logic return developmentMiddleware(request); } }
Next.js middleware is a powerful tool. By using it properly, you can implement complex routing logic, authentication, redirects, and other features while maintaining application performance and security.