Next.js Middleware Chain Design: Authentication, Logging, and Error Handling

ContentQR Team
8 min read
Technical Development
Next.js
Middleware
Authentication
Error Handling

Next.js middleware provides a powerful way to handle cross-cutting concerns like authentication, logging, and error handling. As your application grows, you'll need a systematic approach to manage these concerns without cluttering your route handlers. This guide shows you how to design effective middleware chains in Next.js, implementing authentication, logging, and error handling in a modular and maintainable way. You'll learn how to compose multiple middleware functions, handle errors gracefully, and optimize performance using matchers. Whether you're building a new application or refactoring an existing one, understanding middleware chain patterns will help you create cleaner, more testable code. We'll explore real-world examples and best practices that you can apply immediately to your Next.js projects.

Understanding Next.js Middleware

Next.js middleware runs before a request is completed, allowing you to modify the response, redirect requests, or add headers. It's perfect for implementing authentication, logging, and error handling in a centralized way.

What is Middleware?

Middleware in Next.js is code that runs before a request is completed. It can:

  • Modify request/response headers
  • Redirect requests
  • Rewrite URLs
  • Handle authentication
  • Log requests
  • Handle errors

Middleware File Location

Create middleware.ts in your project root:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Middleware logic here
  return NextResponse.next();
}

export const config = {
  matcher: '/api/:path*',
};

Middleware Chain Design Pattern

Chain of Responsibility

The middleware chain pattern allows you to compose multiple middleware functions that execute in sequence:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

type MiddlewareFunction = (
  request: NextRequest
) => Promise<NextResponse | null>;

async function authMiddleware(request: NextRequest): Promise<NextResponse | null> {
  // Authentication logic
  const token = request.cookies.get('auth-token');
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return null; // Continue to next middleware
}

async function loggingMiddleware(request: NextRequest): Promise<NextResponse | null> {
  // Logging logic
  console.log(`${request.method} ${request.nextUrl.pathname}`);
  return null; // Continue to next middleware
}

async function errorHandlingMiddleware(request: NextRequest): Promise<NextResponse | null> {
  try {
    // Error handling logic
    return null;
  } catch (error) {
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

export async function middleware(request: NextRequest) {
  const middlewares: MiddlewareFunction[] = [
    authMiddleware,
    loggingMiddleware,
    errorHandlingMiddleware,
  ];

  for (const middlewareFn of middlewares) {
    const response = await middlewareFn(request);
    if (response) {
      return response; // Stop chain if middleware returns response
    }
  }

  return NextResponse.next();
}

Authentication Middleware

When implementing authentication middleware, you'll need to handle user sessions and database queries securely. Learn more about Row Level Security best practices to ensure your database queries respect user permissions.

Implementation

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { createClient } from '@/lib/supabase/server';

async function authMiddleware(request: NextRequest): Promise<NextResponse | null> {
  // Protected routes
  const protectedPaths = ['/dashboard', '/qr-generator', '/ai-writer'];
  const isProtectedPath = protectedPaths.some(path => 
    request.nextUrl.pathname.startsWith(path)
  );

  if (!isProtectedPath) {
    return null; // Continue for public routes
  }

  // Check authentication
  const supabase = createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    const redirectUrl = new URL('/login', request.url);
    redirectUrl.searchParams.set('redirect', request.nextUrl.pathname);
    return NextResponse.redirect(redirectUrl);
  }

  return null; // Continue to next middleware
}

Best Practices

1. Early Return Pattern

  • Return early for public routes
  • Only process protected routes
  • Improve performance

2. Redirect with Context

  • Include redirect URL in query params
  • Preserve user intent
  • Better UX

3. Token Validation

  • Validate tokens server-side
  • Check expiration
  • Handle refresh tokens

Logging Middleware

Implementation

async function loggingMiddleware(request: NextRequest): Promise<NextResponse | null> {
  const startTime = Date.now();
  
  // Log request details
  const logData = {
    method: request.method,
    path: request.nextUrl.pathname,
    timestamp: new Date().toISOString(),
    userAgent: request.headers.get('user-agent'),
    ip: request.ip || request.headers.get('x-forwarded-for'),
  };

  console.log('Request:', logData);

  // Add response time header
  const response = NextResponse.next();
  response.headers.set('X-Response-Time', `${Date.now() - startTime}ms`);
  
  return null; // Continue to next middleware
}

Enhanced Logging

async function enhancedLoggingMiddleware(
  request: NextRequest
): Promise<NextResponse | null> {
  const startTime = Date.now();
  
  // Create response with logging
  const response = NextResponse.next();
  
  // Log after response
  response.headers.set('X-Response-Time', `${Date.now() - startTime}ms`);
  
  // Log to external service (async, non-blocking)
  if (process.env.NODE_ENV === 'production') {
    fetch('https://your-logging-service.com/logs', {
      method: 'POST',
      body: JSON.stringify({
        method: request.method,
        path: request.nextUrl.pathname,
        duration: Date.now() - startTime,
      }),
    }).catch(console.error);
  }
  
  return null;
}

Error Handling Middleware

Implementation

async function errorHandlingMiddleware(
  request: NextRequest
): Promise<NextResponse | null> {
  try {
    // Wrap middleware execution
    return null;
  } catch (error) {
    console.error('Middleware error:', error);
    
    // Return appropriate error response
    if (error instanceof Error) {
      return NextResponse.json(
        { 
          error: 'Internal server error',
          message: process.env.NODE_ENV === 'development' 
            ? error.message 
            : undefined,
        },
        { status: 500 }
      );
    }
    
    return NextResponse.json(
      { error: 'Unknown error occurred' },
      { status: 500 }
    );
  }
}

Error Types

class AuthenticationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'AuthenticationError';
  }
}

class AuthorizationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'AuthorizationError';
  }
}

async function errorHandlingMiddleware(
  request: NextRequest
): Promise<NextResponse | null> {
  try {
    // Middleware logic
    return null;
  } catch (error) {
    if (error instanceof AuthenticationError) {
      return NextResponse.json(
        { error: 'Authentication required' },
        { status: 401 }
      );
    }
    
    if (error instanceof AuthorizationError) {
      return NextResponse.json(
        { error: 'Insufficient permissions' },
        { status: 403 }
      );
    }
    
    // Generic error handling
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Complete Middleware Implementation

ContentQR's Middleware Chain

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { createClient } from '@/lib/supabase/server';

type MiddlewareFunction = (
  request: NextRequest
) => Promise<NextResponse | null>;

// 1. Authentication Middleware
async function authMiddleware(request: NextRequest): Promise<NextResponse | null> {
  const protectedPaths = ['/dashboard', '/qr-generator', '/ai-writer'];
  const isProtectedPath = protectedPaths.some(path => 
    request.nextUrl.pathname.startsWith(path)
  );

  if (!isProtectedPath) {
    return null;
  }

  const supabase = createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    const redirectUrl = new URL('/login', request.url);
    redirectUrl.searchParams.set('redirect', request.nextUrl.pathname);
    return NextResponse.redirect(redirectUrl);
  }

  return null;
}

// 2. Logging Middleware
async function loggingMiddleware(request: NextRequest): Promise<NextResponse | null> {
  const startTime = Date.now();
  
  console.log(`${request.method} ${request.nextUrl.pathname}`);
  
  const response = NextResponse.next();
  response.headers.set('X-Response-Time', `${Date.now() - startTime}ms`);
  
  return null;
}

// 3. CORS Middleware (for API routes)
async function corsMiddleware(request: NextRequest): Promise<NextResponse | null> {
  if (request.nextUrl.pathname.startsWith('/api')) {
    const response = NextResponse.next();
    response.headers.set('Access-Control-Allow-Origin', '*');
    response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    return null;
  }
  
  return null;
}

// Main middleware function
export async function middleware(request: NextRequest) {
  const middlewares: MiddlewareFunction[] = [
    authMiddleware,
    loggingMiddleware,
    corsMiddleware,
  ];

  for (const middlewareFn of middlewares) {
    try {
      const response = await middlewareFn(request);
      if (response) {
        return response;
      }
    } catch (error) {
      console.error('Middleware error:', error);
      return NextResponse.json(
        { error: 'Internal server error' },
        { status: 500 }
      );
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    '/dashboard/:path*',
    '/qr-generator/:path*',
    '/ai-writer/:path*',
    '/api/:path*',
  ],
};

Best Practices

1. Middleware Order Matters

Execute middleware in the correct order:

  1. Authentication (early exit if not authenticated)
  2. Logging (log all requests)
  3. Error handling (catch errors)
  4. Business logic

2. Use Matcher for Performance

export const config = {
  matcher: [
    '/dashboard/:path*',
    '/api/:path*',
    // Avoid running on static files
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

3. Early Returns

Return early for routes that don't need processing:

async function authMiddleware(request: NextRequest): Promise<NextResponse | null> {
  // Early return for public routes
  if (request.nextUrl.pathname.startsWith('/blog')) {
    return null;
  }
  
  // Process protected routes
  // ...
}

4. Error Handling

Always wrap middleware in try-catch:

for (const middlewareFn of middlewares) {
  try {
    const response = await middlewareFn(request);
    if (response) return response;
  } catch (error) {
    // Handle error appropriately
    return handleError(error);
  }
}

Testing Middleware

Unit Testing

// __tests__/middleware.test.ts
import { NextRequest } from 'next/server';
import { middleware } from '../middleware';

describe('Middleware', () => {
  it('should redirect unauthenticated users', async () => {
    const request = new NextRequest(
      new URL('http://localhost:3000/dashboard'),
      { headers: {} }
    );
    
    const response = await middleware(request);
    expect(response?.status).toBe(307); // Redirect
  });
});

Conclusion

Designing effective middleware chains in Next.js requires careful planning and understanding of the execution order. By separating concerns into distinct middleware functions, you can create maintainable, testable, and scalable authentication, logging, and error handling systems. The key is to compose middleware functions that each handle a single responsibility, execute them in the correct order, and handle errors gracefully.

Key Takeaways:

  • Use middleware chain pattern for composability and reusability
  • Execute middleware in correct order (authentication → logging → error handling)
  • Handle errors gracefully with proper error types and responses
  • Use matcher configuration for performance optimization
  • Test middleware thoroughly with unit and integration tests

Next Steps:

  • Review your current middleware implementation and identify areas for improvement
  • Implement middleware chain pattern for your authentication flow
  • Add logging middleware to track request performance
  • Set up error handling middleware with proper error types
  • Write tests for your middleware functions

For more Next.js insights, check out our articles on Next.js App Router and Server Components vs Client Components.