Vintotan
Vintotan

Reputation: 41

Chain Multiple Middleware Functions in NextJs 13 for authentication and internationalization

Disclaimer

If you want to have a deeper look into my code this is the repo. Navigate to apps/ott-platform. You have to create an account on Clerk and type in your Clerk key in .env.

Problem

I have a Next.js app (version 13.4.19, app-dir-routing, TypeScript, Turbo). I want to add authentication using Clerk and internationalization using i18next. Therefore, I have to add multiple middleware functions which is at the moment not supported. I figured out that it is possible to create a chain middleware function, so I split my middleware functions in separate files so that the chain middleware function can import them (there is also a YT tutorial).

But I still get the error message:

./middleware.ts:5:39
Type error: Type '(req: NextRequest) => NextResponse<unknown>' is not assignable to type 'MiddlewareFactory'.
Types of parameters 'req' and 'middleware' are incompatible.
Type 'NextMiddleware' is not assignable to type 'NextRequest'.

Authentication is successfully imported but there seems to be a problem in Internationalization. Maybe I did a mistake. I have integrated the middleware function of Internationalization from the example repo of i18next.


This is middleware.ts:

import { chain } from '@/middlewares/chain'
import { Internationalization } from '@/middlewares/internationalization';
import { Authentication } from '@/middlewares/authentication';

export default chain([Authentication, Internationalization])

export const config = {
  matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};

This is chain.ts:


import { NextResponse } from 'next/server'
import type { NextMiddleware } from 'next/server'

type MiddlewareFactory = (middleware: NextMiddleware) => NextMiddleware

export function chain(
  functions: MiddlewareFactory[],
  index = 0
): NextMiddleware {
  const current = functions[index]

  if (current) {
    const next = chain(functions, index + 1)
    return current(next)
  }

  return () => NextResponse.next()
}

This is authentication.ts:

import { authMiddleware } from "@clerk/nextjs";

export const Authentication = () => {
  return authMiddleware({
    publicRoutes: [
      "/(.*)",
      "/signin(.*)",
      "/signup(.*)",
      "/sso-callback(.*)",
    ],
  });
};

This is internationalization.ts:

import { NextResponse, NextRequest } from "next/server";
import acceptLanguage from "accept-language";
import { fallbackLng, languages, cookieName } from "@/app/i18n/settings";

acceptLanguage.languages(languages);

export function Internationalization(req: NextRequest) {
  if (
    req.nextUrl.pathname.indexOf("icon") > -1 ||
    req.nextUrl.pathname.indexOf("chrome") > -1
  )
    return NextResponse.next();
  let lng: string;
  if (req.cookies.has(cookieName))
    lng = acceptLanguage.get(req.cookies.get(cookieName).value);
  if (!lng) lng = acceptLanguage.get(req.headers.get("Accept-Language"));
  if (!lng) lng = fallbackLng;

  // Redirect if lng in path is not supported
  if (
    !languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
    !req.nextUrl.pathname.startsWith("/_next")
  ) {
    return NextResponse.redirect(
      new URL(`/${lng}${req.nextUrl.pathname}`, req.url),
    );
  }

  if (req.headers.has("referer")) {
    const refererUrl = new URL(req.headers.get("referer"));
    const lngInReferer = languages.find((l) =>
      refererUrl.pathname.startsWith(`/${l}`),
    );
    const response = NextResponse.next();
    if (lngInReferer) response.cookies.set(cookieName, lngInReferer);
    return response;
  }

  return NextResponse.next();
}

I want to have a middlware.ts which can handle mutiple middelware functions. I also tried to solve it following the instructions of this thread but it didn't work.

Upvotes: 4

Views: 1844

Answers (1)

Rick van den Broek
Rick van den Broek

Reputation: 197

Implementation

I've recently stumbled upon the same challenge, I've implemented the following solution:

src/middleware.ts

This snippet includes the necessary imports and sets up the final middleware chain and configuration.

import { middleware1 } from './middlewares/middleware1';
import { middleware2 } from './middlewares/middleware2';
import { middlewareHandler } from './middlewares/middlewareHandler';

export const middleware = middlewareHandler([middleware1, middleware2]);

export const config = {
  matcher: ['/((?!api|fonts|_next/static|_next/image|favicon.ico).*)'],
};

src/middlewares/middlewareHandler.ts

Here, the required types and interfaces are imported. middlewareHandler chains multiple middleware wrappers together.

import type { NextRequest, NextFetchEvent, NextResponse } from 'next/server';
import type { MiddlewareWrapperType, ChainMiddlewareType } from './middlewareTypes';

export function middlewareHandler(middlewares: Array<MiddlewareWrapperType>, i = 0): ChainMiddlewareType {
  const current = middlewares[i];

  if (current) {
    const next = middlewareHandler(middlewares, i + 1);

    return current(next);
  }

  return (_req: NextRequest, _evt: NextFetchEvent, res: NextResponse) => {
    return res;
  };
}

src/middlewares/middlewareTypes.ts

Create a new file for shared type definitions.

import type { NextFetchEvent, NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import type { NextMiddlewareResult } from 'next/dist/server/web/types';

export type MiddlewareWrapperType = (middleware: ChainMiddlewareType) => ChainMiddlewareType;

export type ChainMiddlewareType = (
  request: NextRequest,
  event: NextFetchEvent,
  response: NextResponse
) => NextMiddlewareResult | Promise<NextMiddlewareResult>;

src/middlewares/middleware1.ts

This defines middleware1, which creates a default NextResponse and passes control to the next middleware.

import type { NextRequest, NextFetchEvent } from 'next/server';
import { NextResponse } from 'next/server';
import type { MiddlewareWrapperType, ChainMiddlewareType } from './middlewareTypes';

export const middleware1: MiddlewareWrapperType = (next) => {
  return (req, evt) => {
    const res = NextResponse.next();

    return next(req, evt, res);
  };
};

src/middlewares/middleware2.ts

This file defines middleware2, which adds a custom header to the response. But of course you can do whatever you project require

import type { MiddlewareWrapperType, ChainMiddlewareType } from './middlewareTypes';

export const middleware2: MiddlewareWrapperType = (next) => {
  return (req, evt, res) => {
    res.headers.set('x-middleware', 'custom header');

    return next(req, evt, res);
  };
};

Tests

src/middleware/middlewareHandler.test.ts

import type { NextFetchEvent } from 'next/server';
import { NextResponse } from 'next/server';
import { NextRequest } from 'next/server';
import type { MiddlewareWrapperType as MiddlewareWrapperType } from 'middleware/middlewareHandler';
import { middlewareHandler } from 'middleware/middlewareHandler';

describe('middlewareHandler', () => {
  const mockRequest = new Request('https://example.com');
  const mockFetchEvent = {} as NextFetchEvent;
  const mockNextRequest = new NextRequest(mockRequest);
  const mockResponse = new NextResponse();

  it('should call each middleware in the chain once', () => {
    const mockMiddleware1 = jest.fn((next) => next);
    const mockMiddleware2 = jest.fn((next) => next);
    const chained = middlewareHandler([mockMiddleware1, mockMiddleware2]);

    chained(mockNextRequest, mockFetchEvent, mockResponse);

    expect(mockMiddleware1).toHaveBeenCalledTimes(1);
    expect(mockMiddleware2).toHaveBeenCalledTimes(1);
  });

  it('should pass control to the next middleware in the chain', async () => {
    const mockMiddleware1: MiddlewareWrapperType = jest.fn((next) => {
      return (req, evt, resp) => {
        return next(req, evt, resp);
      };
    });

    const mockMiddleware2: MiddlewareWrapperType = jest.fn(() => {
      return () => {
        return new NextResponse(JSON.stringify({ hello: 'world' }));
      };
    });

    const chained = middlewareHandler([mockMiddleware1, mockMiddleware2]);
    const result = await chained(mockNextRequest, mockFetchEvent, mockResponse);
    const json = await result?.json();

    expect(json).toEqual({ hello: 'world' });
  });

  it('should return the response if no middleware is provided', async () => {
    const chained = middlewareHandler([]);
    const result = await chained(mockNextRequest, mockFetchEvent, mockResponse);

    expect(result).toEqual(mockResponse);
  });

  it('should handle asynchronous middleware correctly', async () => {
    const asyncMiddleware: MiddlewareWrapperType = jest.fn((next) => {
      return async (req, evt, resp) => {
        await new Promise((resolve) => setTimeout(resolve, 100));
        return next(req, evt, resp);
      };
    });

    const mockMiddleware = jest.fn((next) => next);
    const chained = middlewareHandler([asyncMiddleware, mockMiddleware]);
    const time = performance.now();

    await chained(mockNextRequest, mockFetchEvent, mockResponse);

    expect(performance.now() - time).toBeGreaterThanOrEqual(100);
    expect(asyncMiddleware).toHaveBeenCalledTimes(1);
    expect(mockMiddleware).toHaveBeenCalledTimes(1);
  });
});

P.s.: My solution is inspired by Hamed Bahram.

Upvotes: 3

Related Questions