Reputation: 41
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
Reputation: 197
I've recently stumbled upon the same challenge, I've implemented the following solution:
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).*)'],
};
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;
};
}
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>;
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);
};
};
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);
};
};
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