Next.jsMiddlewareAuthenticationSecurity

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.

By Vibe Code Basics13 min read
Next.js Middleware: Adding Authentication and More

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

  1. Keep it fast: Middleware runs on every request, so keep it lightweight
  2. Use matchers: Only run middleware on routes that need it
  3. Handle errors: Always handle potential errors gracefully
  4. Cache when possible: Cache expensive operations
  5. 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:

Happy coding! 🚀

Related Articles