How to Chain Multiple Middleware Functions in NextJS
Jasser Mark Arioste
Starting NextJS 12.2, we only define the middleware once inside middleware.ts . Middlewares have a lot of uses in NextJS and if we have multiple middleware logic such as logging, authentication & authorization, split testing, adding cookies, request headers, and checking redirects, should you place them all inside middleware.ts ?
That’s no good.
How do you separate the logic if you have multiple middleware functions? In this tutorial, we’ll do this by using higher-order functions to create middleware functions. We’ll also refactor our code by creating a utility function that uses recursion.
In this tutorial we’ll learn how to organize middleware to that we can configure them from one big middleware function:
...
// one big middleware function
export async function middleware(request:NextRequest) {
const res = NextResponse.next();
const path = request.nextUrl.pathname;
//add headers
res.headers.set("x-content-type-options", "nosniff");
res.headers.set("x-dns-prefetch-control", "false");
res.headers.set("x-download-options", "noopen");
res.headers.set("x-frame-options", "SAMEORIGIN");
if(path == "/cms"){
//check if user is admin
}
//do other middleware operations here
return res;
}
To properly organized middlewares independent of each other:
// middleware.ts
import { stackMiddleware } from "middlewares/stackMiddlewares";
import { withAuthorization } from "middlewares/withAuthorization";
import { withHeaders } from "middlewares/withHeaders";
const middlewares = [withHeaders, withAuthorization];
export default stackMiddleware(middlewares);
Let’s begin!
Pre-requisites
Make sure your NextJS Project has the following:
- NextJS v12.2 or up
- Typescript - Typescript reduces at least 90% of the bugs and helps us better in understanding the code.
The Problem
NextJS allows us to define a middleware function inside the <span>middleware.ts</span> file. They provide us examples of how to create a middleware on specific functionality. But they don’t provide us with how to organize or chain multiple middlewares in a certain way.
To solve this, let’s start by defining a higher-order middleware factory.
Using Higher-Order functions
Higher-order functions are a great way to chain middleware functions. Let’s start by defining a higher-order function. Create a file middlewares/types.ts
// middleware/types.ts
import { NextMiddleware } from "next/server";
export type MiddlewareFactory = (middleware: NextMiddleware) => NextMiddleware;
Explanation: MiddlewareFactory is a function that accepts a NextMiddleware function and returns a NextMiddleware function.
In a concrete implementation, it just wraps the existing middleware with more functionality. For example, suppose you have a middleware that adds some security headers to the response. It would look like this:
// middlewares/withHeaders.ts
import { NextFetchEvent, NextMiddleware, NextRequest } from "next/server";
import { MiddlewareFactory } from "./types";
export const withHeaders: MiddlewareFactory = (next: NextMiddleware) => {
return async (request: NextRequest, _next: NextFetchEvent) => {
const res = await next(request, _next);
if (res) {
res.headers.set("x-content-type-options", "nosniff");
res.headers.set("x-dns-prefetch-control", "false");
res.headers.set("x-download-options", "noopen");
res.headers.set("x-frame-options", "SAMEORIGIN");
}
return res;
};
};
In line 6: Notice that we get the response by calling the next middleware first.
Inside your middleware.ts file, you use it like this:
// middleware.ts
import { withHeaders } from "middlewares/withHeaders";
import { NextResponse } from "next/server";
export function defaultMiddleware() {
return NextResponse.next();
}
export default withHeaders(defaultMiddleware);
//add matchers here if you need to
If you need another middleware like logging, you can create another MiddlewareFactory function:
// middleware/withLogging.ts
import { NextFetchEvent, NextRequest } from "next/server";
import { MiddlewareFactory } from "./types";
export const withLogging: MiddlewareFactory = (next) => {
return async (request: NextRequest, _next: NextFetchEvent) => {
console.log("Log some data here", request.nextUrl.pathname);
return next(request, _next);
};
};
And inside middleware.ts , you can wrap the first two:
// middleware.ts
import { withHeaders } from "middlewares/withHeaders";
import { withLogging } from "middlewares/withLogging";
import { NextResponse } from "next/server";
export function defaultMiddleware() {
return NextResponse.next();
}
export default withLogging(withHeaders(defaultMiddleware))
This approach might be enough in some cases and you can stop here. But if you have multiple middleware functions, you’d end up with something like this:
...
export default middleware1(middleware2(middleware3(middleware4(middleware5(defaultMiddleware)))));
That would be awkward and hard to read.
Creating a Utility function stackMiddlewares
To improve the readability and maintainability of our code, create a file middlewares/stackMiddlewares.ts :
// middlewares/stackMiddlewares
import { NextMiddleware, NextResponse } from "next/server";
import { MiddlewareFactory } from "./types";
export function stackMiddlewares(
functions: MiddlewareFactory[] = [],
index = 0
): NextMiddleware {
const current = functions[index];
if (current) {
const next = stackMiddlewares(functions, index + 1);
return current(next);
}
return () => NextResponse.next();
}
Explanation:
Line 5: We pass an array of MiddlewareFactory functions.
Line 8-11: We get the current middleware factory using the index. If it exists, first we get the next middleware by using recursion. This would go on until the last middleware factory.
Line 12: If there is no middleware factory, we just return a middleware function that returns NextResponse.next()
This would stack the functions together and create the same effect as manually stacking them one by one.
To use it, you can simply do the following:
// middleware.ts
import { stackMiddlewares } from "middlewares/stackMiddlewares";
import { withHeaders } from "middlewares/withHeaders";
import { withLogging } from "middlewares/withLogging";
const middlewares = [withLogging, withHeaders];
export default stackMiddlewares(middleware);
That’s it!
I created a GitHub repo for this tutorial. If you’d like to try it out, you can simply use this command:
npx create-next-app -e https://github.com/jmarioste/next-middleware-guide
Conclusion
We learned how to organize middleware functions by using a higher-order function and we created a utility function to stack the middleware functions for better readability and maintainability.
If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter .
Resources
- The full code is available on GitHub


