Chaining middlewares is a common challenge when building Next.js applications. In this article, I’ll show you how to set up a clean, maintainable solution that avoids common pitfalls.

Base Setup

Middleware chaining isn’t a new problem, and fortunately, there are existing solutions we can build upon. This tutorial provides an excellent foundation. Follow its steps to implement a near-ideal setup as your starting point.

Identifying the Problem

After implementing the tutorial’s solution, you’ll notice that while it works, there are maintainability issues:

  1. Response Initialization in the First Middleware
    The first middleware initializes the NextResponse object using NextResponse.next(). While functional, this approach violates separation of concerns. This task should ideally be handled by the chain function itself.
  2. Fragile Middleware Order
    If you later rearrange the order of middlewares or introduce a new first middleware, you’ll need to modify both the original first middleware and the new one. This introduces unnecessary complexity and potential for bugs.

A Clean and Simple Solution

To address these issues, you can refactor your middleware chain with two simple steps:

1. Create an Initialization Middleware

The response object should be initialized in a dedicated middleware, whose sole responsibility is to create the NextResponse object. This middleware can be added to your chain.ts file as follows:

chain.ts
function initializationMiddleware(middleware: CustomMiddleware) {
	return async (request: NextRequest, event: NextFetchEvent) => {
		const newResponse = NextResponse.next();
		return middleware(request, event, newResponse);
	};
}

2. Add the Initialization Middleware at the Right Moment

Next, ensure that this initialization middleware is always called before any other middleware. You can achieve this by modifying the functions array at the beginning of the chainMiddlewares function, like so:

chain.ts
export function chainMiddlewares(
	functions: MiddlewareFactory[],
	index = 0
): CustomMiddleware {
	if (index === 0) {
	    functions = [initializationMiddleware, ...functions];
	}
	...
}

By doing this, the initialization middleware is automatically added at the start of the chain, simplifying the process.

Benefits of This Approach

  1. Separation of Concerns
    The initialization middleware isolates the response object creation, leaving the remaining middlewares free to focus on their specific logic.
  2. Improved Maintainability
    With this setup, you can rearrange or modify middleware order without worrying about updating response initialization code.
  3. Reduced Code Changes
    Adding new middlewares becomes straightforward, as the response initialization is handled consistently and independently.

Conclusion

By introducing an initialization middleware and ensuring it is always called first, you can significantly improve the maintainability of your Next.js application. This simple yet powerful change allows you to focus on building features without being bogged down by middleware ordering issues.