This RFC proposes a new approach to implementing middleware in Next.js, moving away from a single middleware.ts
file to a more dynamic, route-specific implementation similar to the current handling of dynamic segments, pages, and layouts.
The current middleware implementation in Next.js, while powerful, lacks the flexibility and modularity offered by other Next.js features like dynamic routing. This proposal aims to bring middleware in line with these other features, allowing for more granular control and easier management of middleware across a complex application.
Instead of a single middleware.ts
file at the root of the project, middleware will be implemented using middleware.ts
files located within the directory structure, similar to how pages and layouts are currently handled.
Example structure:
src/
app/
dashboard/
middleware.ts
api/
middleware.ts
middleware.ts # Root middleware
Each middleware.ts
file will export a default function or promise that contains the middleware functionality. The middleware function will now receive three parameters:
import { NextRequest, NextFetchEvent } from 'next/server'
export default function middleware(
request: NextRequest,
event: NextFetchEvent,
context: Map<string, any>
) {
// Middleware logic here
return NextResponse.next()
}
The new context
parameter is a JavaScript Map
that allows middleware functions to share data across the middleware chain. This enables more complex workflows and better separation of concerns.
Example of using the context:
// /src/app/middleware.ts
export default function rootMiddleware(req: NextRequest, evt: NextFetchEvent, context: Map<string, any>) {
context.set('userRole', 'admin');
return NextResponse.next();
}
// /src/app/dashboard/middleware.ts
export default function dashboardMiddleware(req: NextRequest, evt: NextFetchEvent, context: Map<string, any>) {
const userRole = context.get('userRole');
if (userRole !== 'admin') {
return NextResponse.redirect('/login');
}
return NextResponse.next();
}
In this example, the root middleware sets a user role in the context, which is then used by the dashboard middleware to make an access control decision.
By default, a middleware file will apply to all routes under its directory, using a /:path*
matching pattern. For example, /app/dashboard/middleware.ts
would apply to /dashboard
and all its subroutes.
To allow for exact path matching, middleware files can export an exactPathMatching
constant:
export const exactPathMatching = true
When exactPathMatching
is set to true
, the middleware will only be applied to the exact path where it's located. For example, if /app/dashboard/middleware.ts
exports exactPathMatching = true
, it will only apply to the /dashboard
route and not to any subroutes.
If the exactPathMatching
export is omitted or set to false
, the default behavior (matching all subroutes) will be used.
Middleware will be applied in order from the root to the most specific path. This allows for composing middleware functionality across different levels of the application.
When there are multiple middleware functions (either in the same file or in nested directories), they will be chained in order. The chain continues as long as each middleware function returns NextResponse.next()
. If a middleware function returns anything else (such as a redirect or an error response), the chain will be broken, and that response will be sent to the client.
The context
object is shared across all middleware functions in the chain, allowing them to pass data to each other. This context persists only for the duration of the current request and is not shared between different requests.
Crucially, if the last middleware in the chain returns NextResponse.next()
, the middleware will return the response as modified by the entire chain. This allows each middleware in the chain to modify the request or response, with the final result incorporating all these modifications.
Example of chaining with context:
// /src/app/middleware.ts
export default function rootMiddleware(req: NextRequest, evt: NextFetchEvent, context: Map<string, any>) {
console.log('Root middleware executed');
context.set('executionStart', Date.now());
const response = NextResponse.next();
response.headers.set('X-Root-Header', 'root-value');
return response;
}
// /src/app/dashboard/middleware.ts
export const exactPathMatching = true;
export default function dashboardMiddleware(req: NextRequest, evt: NextFetchEvent, context: Map<string, any>) {
console.log('Dashboard middleware executed');
const executionStart = context.get('executionStart');
const executionTime = Date.now() - executionStart;
context.set('executionTime', executionTime);
const response = NextResponse.next();
response.headers.set('X-Dashboard-Header', 'dashboard-value');
response.headers.set('X-Execution-Time', `${executionTime}ms`);
return response;
}
In this example, the root middleware sets an execution start time, which is then used by the dashboard middleware to calculate and set the total execution time.
To enhance the developer experience and make it easier to understand and debug middleware execution, we propose the following additions:
We will add a new section to the Vercel toolbar specifically for middleware debugging. This tool will:
- Display a list of all middleware functions that were executed for the currently opened route.
- Show the order of execution and the file path of each middleware.
- Indicate whether each middleware function returned
NextResponse.next()
or broke the chain. - Provide a summary of any modifications made to the request or response by each middleware.
Example toolbar display:
Middleware Execution:
1. /src/app/middleware.ts (continued)
- Added header: X-Root-Header
- Context: Set 'executionStart'
2. /src/app/dashboard/middleware.ts (continued)
- Added header: X-Dashboard-Header
- Added header: X-Execution-Time
- Context: Used 'executionStart', Set 'executionTime'
3. /src/app/dashboard/users/middleware.ts (chain end)
- Redirected to: /login
- Context: Used 'executionTime'
We will add a new experimental flag in Next.js configuration to enable more granular middleware execution logging. When enabled, this will provide detailed logs in the Next.js console, showing:
- Each middleware function that was considered for execution.
- Whether the middleware was skipped (due to path matching rules) or executed.
- The return value of each executed middleware (e.g.,
NextResponse.next()
, redirect, etc.). - Any modifications made to the request or response.
- The final composed response after all middleware executions.
- Context operations performed by each middleware.
Example console output:
[Middleware] Request: GET /dashboard/users
[Middleware] Executing: /src/app/middleware.ts
Result: NextResponse.next() with modifications
- Added header: X-Root-Header
- Context: Set 'executionStart'
[Middleware] Executing: /src/app/dashboard/middleware.ts
Result: NextResponse.next() with modifications
- Added header: X-Dashboard-Header
- Added header: X-Execution-Time
- Context: Used 'executionStart', Set 'executionTime'
[Middleware] Skipped: /src/app/dashboard/profile/middleware.ts (path mismatch)
[Middleware] Executing: /src/app/dashboard/users/middleware.ts
Result: Redirect to /login
- Context: Used 'executionTime'
[Middleware] Chain ended. Final result: Redirect to /login
These debugging tools will be invaluable for developers working with the new middleware system, allowing them to easily trace the execution path and understand how each middleware function affects the request and response.
To ensure backwards compatibility:
- The existing root
middleware.ts
file will continue to work as before. - A migration guide will be provided to help developers transition to the new system.
- A codemod tool will be developed to assist in automatically refactoring existing middleware implementations.
- Modify the Next.js build process to recognize and compile
middleware.ts
files throughout the project structure. - Update the routing system to apply middleware based on file location and the
exact
export. - Implement a system for composing and chaining multiple middleware functions, ensuring that modifications from all middleware in the chain are preserved in the final response.
- Create necessary TypeScript types and update existing types to support the new system.
- Develop the Middleware Debugger component for the Vercel toolbar, integrating it with the existing toolbar architecture.
- Implement the enhanced console logging system, including the new experimental flag in the Next.js configuration.
- Create APIs or hooks that allow middleware functions to report their execution details and modifications for debugging purposes.
- Implement the
context
parameter as aMap
object that is created for each request and passed through the middleware chain.
- How should conflicts between middleware at different levels be resolved?
- Should there be a way to disable middleware inheritance for certain routes?
- How can we optimize performance when multiple middleware files are present?
- Should we provide a mechanism to explicitly break the middleware chain from within a middleware function?
- Should we provide built-in utilities or helper functions for common context operations, such as type-safe getters and setters?
- Middleware-specific DevTools for easier debugging and performance monitoring.
- A library of common middleware functions that can be easily imported and composed.
- Integration with other Next.js features like ISR and API Routes.
- Advanced middleware chaining controls, such as conditional execution or parallel processing of certain middleware functions.
- Interactive middleware debugging, allowing developers to modify and re-run middleware functions in real-time during development.
- Performance profiling for middleware, helping developers identify and optimize slow middleware functions.
- Integration with popular browser developer tools, providing middleware execution information directly in the browser's network tab.
- Typed contexts: Provide a way for developers to define and use typed contexts for improved type safety when sharing data between middleware functions.
Feedback
I would rename the
exact
export into something more explicit, likeexactPathMatching
. Does setting it also bypass the parent middleware?Isn't that done by returning a
NextResponse
at any point in the chain? CallingNextResponse.next()
moves execution down the tree, but returning a response marks it as done, similar to what is done in the current flat middleware implementation.Edit: sorry I had not yet been to the composition part, with execution capturing down, then bubbling up (a bit like DOM events). Maybe a similar API would work, by throwing some built-in functions to mark end of execution?
This is critical IMHO.