Have you ever wondered how to use multiple middleware functions in Next.js when it only exports one function? In this tutorial, I'll guide you through implementing a modular middleware architecture that combines authentication with NextAuth v5 and internationalization using Next-Intl.
By the end of this article, you'll have a clean, maintainable approach to chaining middleware that you can easily extend for additional functionality.
Let's dive in! 🚀
The Challenge
Next.js middleware is incredibly powerful, but there's a limitation: you can only export one middleware function. This creates a challenge when you need multiple middleware capabilities like:
- Authentication checks
- Language detection and routing
- Logging or analytics
- Rate limiting
How do we solve this elegantly? By creating a middleware chain that preserves modularity while meeting Next.js requirements.
Our Approach
We'll create:
- A utility to chain multiple middleware functions
- Individual middleware modules for authentication and internationalization
- A clean way to combine them in the correct order
File Structure
First, let's understand our project organization:
/my-next-app
|-- /src
| |-- /middlewares
| | |-- chain.ts
| | |-- withI18nMiddleware.ts
| | `-- withAuthMiddleware.ts
| |-- auth.config.ts
| |-- auth.ts
| |-- middleware.ts
| |-- i18n.config.ts
`-- next.config.js
Step 1: Creating the Chain Utility
The chain utility is the foundation of our solution. It takes an array of middleware functions and returns a single function that executes them in sequence.
// middlewares/chain.ts
import { NextMiddlewareResult } from 'next/dist/server/web/types';
import { NextResponse } from 'next/server';
import type { NextFetchEvent, NextRequest } from 'next/server';
export type CustomMiddleware = (
request: NextRequest,
event: NextFetchEvent,
response: NextResponse
) => NextMiddlewareResult | Promise<NextMiddlewareResult>;
type MiddlewareFactory = (middleware: CustomMiddleware) => CustomMiddleware;
export function chain(
functions: MiddlewareFactory[],
index = 0
): CustomMiddleware {
const current = functions[index];
if (current) {
const next = chain(functions, index + 1);
return current(next);
}
return (request: NextRequest, event: NextFetchEvent, response: NextResponse) => {
return response;
};
}
This recursive implementation allows each middleware to decide whether to continue the chain or terminate early by returning a response.
Step 2: Internationalization Middleware
Next, let's implement a middleware that handles language detection and routing:
// middlewares/withI18nMiddleware.ts
import { NextResponse } from 'next/server';
import type { NextFetchEvent, NextRequest } from 'next/server';
import { i18n } from '@/i18n.config';
import { match as matchLocale } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
import { CustomMiddleware } from './chain';
function getLocale(request: NextRequest): string | undefined {
const negotiatorHeaders: Record<string, string> = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
// @ts-ignore locales are readonly
const locales: string[] = i18n.locales;
const languages = new Negotiator({ headers: negotiatorHeaders }).languages();
const locale = matchLocale(languages, locales, i18n.defaultLocale);
return locale;
}
export function withI18nMiddleware(middleware: CustomMiddleware) {
return async (
request: NextRequest,
event: NextFetchEvent,
response: NextResponse
) => {
const pathname = request.nextUrl.pathname;
const pathnameIsMissingLocale = i18n.locales.every(
locale => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);
if (pathnameIsMissingLocale) {
const locale = getLocale(request)
const redirectURL = new URL(request.url)
if (locale) {
redirectURL.pathname = `/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`
}
// Preserve query parameters
redirectURL.search = request.nextUrl.search
return NextResponse.redirect(redirectURL.toString())
}
return middleware(request, event, response);
};
}
This middleware detects the user's preferred language and ensures URLs include the appropriate locale prefix.
Step 3: Authentication Middleware
Now for our authentication middleware using NextAuth v5:
// middlewares/withAuthMiddleware.ts
import { getToken } from 'next-auth/jwt';
import { NextResponse } from 'next/server';
import type { NextFetchEvent, NextRequest } from 'next/server';
import { CustomMiddleware } from './chain';
export function withAuthMiddleware(middleware: CustomMiddleware): CustomMiddleware {
return async (request: NextRequest, event: NextFetchEvent, response: NextResponse) => {
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });
if (!token) {
return NextResponse.redirect(new URL('/api/auth/signin', request.url));
}
return middleware(request, event, response);
};
}
This middleware checks for a valid authentication token and redirects unauthenticated users to the sign-in page.
Step 4: Combining Everything
Finally, let's put it all together in our main middleware file:
// middleware.ts
import { chain } from '@/middlewares/chain';
import { withI18nMiddleware } from '@/middlewares/withI18nMiddleware';
import { withAuthMiddleware } from '@/middlewares/withAuthMiddleware';
export default chain([withI18nMiddleware, withAuthMiddleware]);
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|images).*)'],
};
Note the order matters! Here, internationalization runs first, followed by authentication. This ensures we handle locale routing before authentication checks.
The Benefits of This Approach
Our middleware chain architecture provides several advantages:
- Modularity: Each middleware focuses on a single responsibility
- Extensibility: Add new middleware by creating a module and adding it to the chain
- Maintainability: Logic is isolated, making debugging and updates easier
- Reusability: Middleware can be reused across projects
Conclusion
With this architecture in place, you can easily combine multiple middleware functions in Next.js while maintaining clean, modular code. This approach scales beautifully as your application grows in complexity.
You could extend this further with middleware for:
- Analytics and logging
- Rate limiting
- Feature flags
- A/B testing
What other middleware would you add to your Next.js applications? Let me know in the comments!
Top comments (0)