Next.js Middleware Chain Design: Authentication, Logging, and 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:
- Authentication (early exit if not authenticated)
- Logging (log all requests)
- Error handling (catch errors)
- 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.
Related Posts
ContentQR Full-Stack Architecture Evolution: From Monolith to Modular Design
Learn how to evolve your architecture from monolith to modular design. Practical insights and lessons learned from real-world experience.
Advanced Type Handling: Generics and Utility Types Usage Tips
Master advanced TypeScript type handling with generics and utility types. Learn practical tips and patterns for complex type scenarios.
Next.js App Router Best Practices: Migration from Pages Router
Sharing our experience migrating ContentQR from Pages Router to App Router, including best practices and lessons learned.