Next.js Middleware: Adding Authentication and More
Learn how to use Next.js middleware for authentication, redirects, request modification, and more. Build secure, performant applications.
Next.js Middleware: Adding Authentication and More
Middleware in Next.js allows you to run code before a request is completed. It's perfect for authentication, redirects, request modification, and more. Let's explore how to use it effectively.
If you're new to Next.js, check out our Getting Started with Next.js guide.
What is Middleware?
Middleware runs before a request is completed. You can:
- Modify the request or response
- Redirect users
- Add headers
- Check authentication
- Rewrite URLs
- And much more
Creating Middleware
Create a middleware.js (or middleware.ts) file in your project root:
// middleware.js
import { NextResponse } from 'next/server';
export function middleware(request) {
// Your middleware logic here
return NextResponse.next();
}
export const config = {
matcher: '/about/:path*',
};
Basic Authentication Example
Here's a simple authentication middleware:
// middleware.js
import { NextResponse } from 'next/server';
export function middleware(request) {
const token = request.cookies.get('auth-token');
// Protect dashboard routes
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
// Redirect authenticated users away from login
if (request.nextUrl.pathname.startsWith('/login')) {
if (token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
};
Advanced Authentication
Here's a more robust authentication example:
// middleware.js
import { NextResponse } from 'next/server';
export async function middleware(request) {
const token = request.cookies.get('auth-token')?.value;
const { pathname } = request.nextUrl;
// Public routes that don't require authentication
const publicRoutes = ['/login', '/register', '/'];
const isPublicRoute = publicRoutes.includes(pathname);
// Protected routes
const isProtectedRoute = pathname.startsWith('/dashboard') ||
pathname.startsWith('/admin');
// Check authentication
if (isProtectedRoute && !token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname);
return NextResponse.redirect(loginUrl);
}
// Verify token if present
if (token) {
try {
const response = await fetch(`${process.env.API_URL}/verify-token`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (!response.ok && isProtectedRoute) {
return NextResponse.redirect(new URL('/login', request.url));
}
} catch (error) {
console.error('Token verification failed:', error);
}
}
return NextResponse.next();
}
export const config = {
matcher: [
'/dashboard/:path*',
'/admin/:path*',
'/login',
'/register',
],
};
Redirects
Middleware is perfect for handling redirects:
import { NextResponse } from 'next/server';
export function middleware(request) {
const { pathname } = request.nextUrl;
// Redirect old URLs to new ones
if (pathname === '/old-page') {
return NextResponse.redirect(new URL('/new-page', request.url));
}
// Redirect based on locale
const locale = request.headers.get('accept-language')?.split(',')[0];
if (pathname === '/' && locale?.startsWith('es')) {
return NextResponse.redirect(new URL('/es', request.url));
}
return NextResponse.next();
}
Adding Headers
Add custom headers to requests:
import { NextResponse } from 'next/server';
export function middleware(request) {
const response = NextResponse.next();
// Add custom headers
response.headers.set('X-Custom-Header', 'Custom Value');
response.headers.set('X-Request-ID', crypto.randomUUID());
// Security headers
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
return response;
}
URL Rewriting
Rewrite URLs without changing what the user sees:
import { NextResponse } from 'next/server';
export function middleware(request) {
const { pathname } = request.nextUrl;
// Rewrite /api/user to /api/users/me
if (pathname === '/api/user') {
return NextResponse.rewrite(new URL('/api/users/me', request.url));
}
return NextResponse.next();
}
A/B Testing
Use middleware for A/B testing:
import { NextResponse } from 'next/server';
export function middleware(request) {
const { pathname } = request.nextUrl;
if (pathname === '/') {
const variant = Math.random() > 0.5 ? 'a' : 'b';
const response = NextResponse.next();
response.cookies.set('ab-variant', variant);
if (variant === 'b') {
return NextResponse.rewrite(new URL('/home-variant-b', request.url));
}
}
return NextResponse.next();
}
Rate Limiting
Implement rate limiting in middleware:
import { NextResponse } from 'next/server';
const rateLimitMap = new Map();
export function middleware(request) {
const ip = request.ip || 'unknown';
const now = Date.now();
const windowMs = 60 * 1000; // 1 minute
const maxRequests = 10;
if (!rateLimitMap.has(ip)) {
rateLimitMap.set(ip, { count: 1, resetTime: now + windowMs });
return NextResponse.next();
}
const record = rateLimitMap.get(ip);
if (now > record.resetTime) {
record.count = 1;
record.resetTime = now + windowMs;
return NextResponse.next();
}
if (record.count >= maxRequests) {
return new NextResponse('Too Many Requests', { status: 429 });
}
record.count++;
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*',
};
Geolocation-Based Redirects
Redirect based on user location:
import { NextResponse } from 'next/server';
export function middleware(request) {
const country = request.geo?.country || 'US';
const { pathname } = request.nextUrl;
// Redirect EU users to EU-specific pages
const euCountries = ['DE', 'FR', 'IT', 'ES', 'NL'];
if (euCountries.includes(country) && pathname === '/') {
return NextResponse.redirect(new URL('/eu', request.url));
}
return NextResponse.next();
}
Matcher Configuration
Control which routes middleware runs on:
export const config = {
// Single path
matcher: '/about',
// Multiple paths
matcher: ['/about', '/dashboard/:path*'],
// Exclude paths
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
Best Practices
- Keep it fast: Middleware runs on every request, so keep it lightweight
- Use matchers: Only run middleware on routes that need it
- Handle errors: Always handle potential errors gracefully
- Cache when possible: Cache expensive operations
- Test thoroughly: Middleware affects all matching routes
Common Patterns
Role-Based Access Control
export async function middleware(request) {
const token = request.cookies.get('auth-token');
const userRole = await getUserRole(token);
if (request.nextUrl.pathname.startsWith('/admin')) {
if (userRole !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
}
return NextResponse.next();
}
Maintenance Mode
export function middleware(request) {
if (process.env.MAINTENANCE_MODE === 'true') {
return NextResponse.rewrite(new URL('/maintenance', request.url));
}
return NextResponse.next();
}
Conclusion
Next.js middleware is a powerful tool for handling authentication, redirects, and request modification. By using it effectively, you can build more secure and performant applications.
Next Steps:
- Learn about Next.js Routing
- Explore API Routes
- Discover Image Optimization
- Check out Metadata API for SEO
Happy coding! 🚀
Related Articles
Next.js API Routes: Creating Backend Endpoints
Build powerful backend endpoints with Next.js API routes. Learn how to handle GET, POST, PUT, DELETE requests and create RESTful APIs.
Next.js Image Optimization: Using the Image Component
Master Next.js image optimization with the Image component. Learn lazy loading, responsive images, and performance best practices.
Next.js Metadata API: Improving SEO
Learn how to use Next.js Metadata API to improve SEO, add Open Graph tags, Twitter cards, and structured data to your pages.