ReactHustle

How to Chain Multiple Middleware Functions in NextJS

Jasser Mark Arioste

Jasser Mark Arioste

How to Chain Multiple Middleware Functions in NextJS

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:

  1. NextJS v12.2 or up
  2. 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 #

  1. The full code is available on GitHub
Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle