Skip to content
Go back

Chaining middlewares in Next.js

In this post, I’ll introduce a powerful pattern for managing multiple middleware functions in a Next.js application. This approach allows you to separate your logic into distinct, manageable, and reusable middleware modules, bringing order to what can often become a chaotic part of a growing project.

Motivation

Next.js is a fantastic framework, but its middleware implementation has a notable limitation: you can only have one middleware.ts file. In a small project, this is manageable. But as your application scales, you’ll likely need to handle various cross-cutting concerns like authentication, A/B testing, internationalization (i18n) redirects, analytics, and security headers.

Placing all this logic into a single file quickly leads to a maintenance nightmare. The file becomes bloated, difficult to read, and prone to bugs. This violates the single-responsibility principle and makes it challenging for teams to work on different middleware tasks concurrently without causing conflicts.

The main motivation for this blog post is to demonstrate a clean, scalable solution. By creating a “chaining” function, you can define each piece of middleware logic in its own file and then compose them together in a clear and predictable order. This pattern promotes better code organization, reusability, and maintainability, allowing you to build more robust Next.js applications.

The Solution: A Chaining Utility

To solve this, we can implement a “chaining” function. This approach allows us to define each piece of middleware logic in its own function and then compose them together into a single chain that Next.js can execute.

Here is the core utility that makes this possible. It consists of a type definition for our factories and a function to chain them.

// src/types/middleware-factory.ts
import { NextMiddleware } from 'next/server';

export type MiddlewareFactory = (middleware: NextMiddleware) => NextMiddleware;
// src/utils/chainMiddlewares.ts
import { MiddlewareFactory } from '../types/middleware-factory';
import { NextMiddleware, NextResponse } from 'next/server';

export function chainMiddlewares(
  functions: MiddlewareFactory[] = [],
  index = 0
): NextMiddleware {
  const current = functions[index];
  if (current) {
    const next = chainMiddlewares(functions, index + 1);
    return current(next);
  }
  return () => NextResponse.next();
}

How It Works

The chainMiddlewares function is a recursive utility that builds a chain of middleware functions. Here’s a breakdown:

  1. It takes an array of MiddlewareFactory functions. A MiddlewareFactory is a higher-order function that accepts a NextMiddleware function (we’ll call it next) and returns a new NextMiddleware function.
  2. It starts from the first factory (index = 0).
  3. For each factory, it recursively calls itself to get the next middleware in the chain.
  4. It then calls the current factory, passing the next middleware to it. This “wraps” the next middleware inside the current one.
  5. The recursion stops when there are no more factories in the array. At this point, it returns a default middleware that simply calls NextResponse.next(), terminating the chain.

Creating and Using Middleware Factories

With the chaining utility in place, let’s create two examples: one for handling authentication and another for adding Content Security Policy (CSP) headers.

1. Authentication Middleware

This middleware checks for an authentication token and redirects to a login page if it’s missing.

// src/middlewares/withAuth.ts
import { MiddlewareFactory } from '@/types/middleware-factory';
import { NextRequest, NextResponse } from 'next/server';

export const withAuth: MiddlewareFactory = (next) => {
  return async (request: NextRequest, _next) => {
    const pathname = request.nextUrl.pathname;

    // Skip auth for public paths
    if (pathname.startsWith('/login') || pathname.startsWith('/api')) {
      return next(request, _next);
    }

    const token = request.cookies.get('auth_token');

    if (!token) {
      const url = new URL('/login', request.url);
      return NextResponse.redirect(url);
    }

    // If authenticated, continue to the next middleware
    return next(request, _next);
  };
};

2. Content Security Policy (CSP) Middleware

This middleware adds security headers to protect your application against cross-site scripting (XSS) and other injection attacks.

// src/middlewares/withCsp.ts
import { MiddlewareFactory } from '@/types/middleware-factory';
import { NextRequest, NextResponse } from 'next/server';

export const withCsp: MiddlewareFactory = (next) => {
  return async (request: NextRequest, _next) => {
    // First, call the next middleware in the chain
    const response = await next(request, _next);

    if (response) {
      const cspHeader = `
        default-src 'self';
        script-src 'self' 'unsafe-eval' 'unsafe-inline';
        style-src 'self' 'unsafe-inline';
        img-src 'self' blob: data:;
        font-src 'self';
        object-src 'none';
        base-uri 'self';
        form-action 'self';
        frame-ancestors 'none';
        upgrade-insecure-requests;
      `;

      // Set the CSP header on the response
      response.headers.set(
        'Content-Security-Policy',
        cspHeader.replace(/\s{2,}/g, ' ').trim() // Replace newlines and multiple spaces
      );
    }

    return response;
  };
};

Chaining Them Together

Finally, in your main middleware.ts file, you import your chaining function and the individual middleware factories, then compose them.

// src/middleware.ts
import { chainMiddlewares } from './utils/chainMiddlewares';
import { withAuth } from './middlewares/withAuth';
import { withCsp } from './middlewares/withCsp';

const middlewares = [withAuth, withCsp];

export default chainMiddlewares(middlewares);

export const config = {
  // Match all request paths except for the ones starting with:
  // - _next/static (static files)
  // - _next/image (image optimization files)
  // - favicon.ico (favicon file)
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

The order in the middlewares array matters. In this example, withAuth will run first. If it decides to redirect, withCsp will never be executed. The CSP headers will only be applied to responses that make it through the entire chain.

Final Considerations

This pattern provides a clean and scalable way to manage your Next.js middlewares. The key benefits are:

By adopting this simple yet powerful pattern, you can keep your Next.js projects organized and robust, no matter how complex your middleware logic becomes.


Share this post on:

Next Post
How to use link-prevue with an external API