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; }
1234567891011121314151617181920
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);
12345678
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 middleware.ts
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;
1234
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; }; };
123456789101112131415
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
123456789
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); }; };
123456789
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))
12345678
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)))));
12
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(); }
1234567891011121314
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);
1234567
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
1
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