Implementing Multiple Middleware in Next.js: Combining NextAuth V(5) and Internationalization
Published: 2024-05-21 14:05:09


<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Implementing Multiple Middleware Functions in Next.js</title>
</head>

<body>
  <h1>Welcome to my first blog post on Dev Community </h1>
  <p>Today, I'll walk you through implementing multiple middleware functions in a Next.js application. Specifically, we'll combine authentication using NextAuth (v5) and internationalization. By the end of this tutorial, you'll know how to chain middleware functions to create a modular and maintainable setup.</p>
  <p>
    We know next js middleware just export one funtion. But what if we want to use multiple middleware functions in our Next.js application? In this tutorial, we'll show you how to combine multiple middleware functions using a chain utility. We'll create middleware for internationalization and authentication, and then chain them together to create a single middleware function. This approach allows us to easily add more middleware functions in the future without cluttering our codebase. You can use this setup to add additional features like logging, caching, or error handling to your Next.js application.
  </p>

  <h2>
    Let's get started! 🚀 
  </h2>

  <h2>File Structure</h2>
  <p>Before diving into the code, let's set up our project structure to keep everything organized. Here's a simplified version of our file structure:</p>

  <pre><code>/my-next-app
|-- /src
|   |-- /middlewares
|   |   |-- chain.ts
|   |   |-- withI18nMiddleware.ts
|   |   `-- withAuthMiddleware.ts
|   |-- auth.config.ts
|   |-- auth.ts
|   |-- middleware.ts
|   |-- i18n.config.ts
|   
`-- next.config.js
</code></pre>

<br/>

  <h2>Step 1: Setting Up the Chain Utility</h2>
  <p>The first step is to create a utility to chain multiple middleware functions. This utility will ensure that each middleware function is executed in sequence. <b>We'll use this utility to combine the internationalization and authentication middlewares. </b>  This will allow us to easily add more middleware functions in the future.  
</p>

<p>
  The chain utility takes an array of middleware functions and returns a single middleware function that executes each middleware in order. If a middleware function returns a response, the chain stops and returns that response. Otherwise, it calls the next middleware function in the chain. It continues this process until all middleware functions have been executed. So let's create the chain utility.
</p>

  <pre><code>Create a chain.ts file in the /middlewares directory</code></pre>

  <p>Here's the content of the chain.ts file:</p>

  <pre><code> &lt;code&gt;
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
) =&gt; NextMiddlewareResult | Promise&lt;NextMiddlewareResult&gt;;

type MiddlewareFactory = (middleware: CustomMiddleware) =&gt; 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
  ) =&gt; {
    return response;
  };
}
&lt;/code&gt;</code></pre>

<br/>

  <h2>Step 2: Creating the Internationalization Middleware</h2>
  <p>Next, we'll create middleware for handling internationalization. This middleware will detect the user's locale and redirect them if necessary.</p>
  <p>
    We'll use the @formatjs/intl-localematcher library to match the user's preferred languages with the available locales in our application. If the user's preferred language is not supported, we'll redirect them to the default locale. This ensures that the user always sees content in a supported language. 
  </p>
  
  <pre><code> Create a withI18nMiddleware.ts file in the /middlewares directory </code></pre>

  <p>Here's the content of the withI18nMiddleware.ts file:</p>

  <pre><code>&lt;code&gt;
// 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&lt;string, string&gt; = {};
  request.headers.forEach((value, key) =&gt; (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
  ) =&gt; {
    const pathname = request.nextUrl.pathname;
    const pathnameIsMissingLocale = i18n.locales.every(
      locale =&gt; !pathname.startsWith(`/${locale}/`) &amp;&amp; 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);
  };
}
&lt;/code&gt;</code></pre>

<br/>

  <h2>Step 3: Creating the Authentication Middleware</h2>
  <p>Now, let's create middleware for handling authentication using NextAuth.</p>
  <p>
    We'll use the getToken function from next-auth/jwt to retrieve the user's token. If the token is missing, we'll redirect the user to the sign-in page. This ensures that only authenticated users can access protected routes in our application.
  </p>
  <p>
    Note: Make sure to set the NEXTAUTH_SECRET environment variable in your Next.js application. You can generate a secure secret using a tool like openssl or a password manager.
  </p>

  <pre><code> Create a withAuthMiddleware.ts file in the /middlewares directory </code></pre>

  <p>Here's the content of the withAuthMiddleware.ts file:</p>

  <pre><code>&lt;code&gt;
// 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) =&gt; {
    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);
  };
}
&lt;/code&gt;</code></pre>

  <br/>

  <h2>Step 4: Combining the Middlewares</h2>
  <p>Finally, we'll chain the internationalization and authentication middlewares together in a single middleware file. We combine them using the chain utility we created earlier. Now we can use this middleware in our Next.js application.
  </p>
  <p>
    In main middleware file, we can define the order of the middleware functions , remember order is important. If you want to change the order of the middleware functions, you can simply rearrange them in the chain array. You need to make sure what middleware function should be executed first and what should be executed later.
  </p>
  <p>
    In this example, we're using the withI18nMiddleware first to handle internationalization and then withAuthMiddleware to handle authentication. You can add more middleware functions as needed.
  </p>
<br />

  <pre><code> Create a middleware.ts file at the root of your project </code></pre>

  <p>Here's the content of the middleware.ts file:</p>

  <pre><code>&lt;code&gt;
// 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).*)'],
};
&lt;/code&gt;</code></pre>

<br/>

  <h2>Conclusion</h2>


  <p>By following these steps, you've successfully combined NextAuth and internationalization middleware in your Next.js application. This modular approach allows you to add more middleware functions as needed without cluttering your codebase. You can now easily extend your application with additional features while keeping your code clean and maintainable.</p>

  <p>Thanks for reading! I hope you found this tutorial helpful. If you have any questions or feedback, feel free to leave a comment below.</p>
</body>

</html>