Protecting Routes in Next.js 13

With the evolution of the App Router in Next.js, the framework has become even more robust, offering developers enhanced control over…

With the evolution of the App Router in Next.js, the framework has become even more robust, offering developers enhanced control over routing and the ability to protect routes more efficiently. This article will delve deep into how to protect routes in Next.js using the App Router (Next 13.4), covering the latest features such as Middleware, exportMode, unstable_cache, and more. This guide is intended for senior developers with a good understanding of Next.js and React.

This article is for personal reference— your mileage may vary!

Complete examples can be found in the associated Gist. You can find them here:https://gist.github.com/zachshallbetter.

The App Router

Next.js is a popular framework for building React applications that support server-side rendering, static site generation, and incremental static regeneration. One of the features that Next.js provides is the App component, which allows you to customize the rendering of your pages and share state across them.

The App Router is a feature in Next.js that allows developers to handle routing more centrally and efficiently. It provides a single place (the app directory) to define routes and their corresponding components. This makes it easier to manage routes and allows for advanced routing features like transitions, nesting, and route protection.

Changes in Configuration

Replacing output with exportMode

The output option in next.config.js is deprecated and replaced by exportMode. Here's how you can use the new option:

module.exports = {
  exportMode: 'your-value',
};

Replacing generateStaticParams with getStaticPaths

The generateStaticParams function is no longer required and replaced by getStaticPaths. Here's how you can use the new function:

export async function getStaticPaths() {
  // Your logic here
}

Replacing fetch with unstable_cache

The fetch function is no longer automatically memoized and replaced by unstable_cache. Here's how you can use the new feature:

import { unstable_cache } from 'next/fetch';

export async function getServerSideProps() {
  const data = await unstable_cache('your-api-url');
  return { props: { data } };
}

Replacing revalidateTag with revalidate

The revalidateTag function is no longer available and has been replaced by revalidate. Here's how you can use the new function:

export async function getStaticProps() {
  // Your logic here
  return {
    revalidate: 10, // Time in seconds
  };
}

Protecting Pages, Components, and APIs with Middleware

The Route Handlers feature is no longer supported and replaced by Middleware. Here's how you can use Middleware to protect pages, components, and APIs:

import { NextResponse } from 'next/server';

export function authMiddleware(req) {
  // Add your own logic here to check if the user is authenticated
  const userIsAuthenticated = checkUserAuthentication();

  if (!userIsAuthenticated) {
    return NextResponse.redirect('/login');
  }

  return NextResponse.next();
}

In this example, we define an authMiddleware function that checks if the user is authenticated. If not, it redirects them to the login page. We then import this function into our pages and API routes and apply it using authMiddleware(req).

Using the App Component to Protect Routes in Next.js 13.4

The App component is a special component that Next.js uses to initialize pages. It receives props such as Component, pageProps, router, and err, and it can return any React element. The Component prop is the active page component, and the pageProps prop is the props that are passed to it. The router prop is an instance of the Next.js router, which exposes methods and properties such as push, replace, pathname, query, and asPath. The err prop is an optional error object that is only defined when an error occurs during rendering.

One of the benefits of using the App component is that it allows you to wrap your pages with a common layout or a provider component. For example, you can use the App component to add a header and a footer to all your pages, or to provide a global context or a theme to your application. To use the App component, you need to create a file called _app.js (or _app.tsx if you are using TypeScript) in the pages directory and export a default function or class component.

To protect routes that require authentication or authorization, we can use the App component to check the user’s status and redirect them to a login page or an error page if they are not authorized to access the current page. To do this, we need to use two hooks: useRouter and useEffect. The useRouter hook returns the router object that we can use to access the current route information and perform navigation. The useEffect hook allows us to execute a side effect after the component renders.

The basic idea is to use useEffect to check the user's status and compare it with the required status for the current route. If they don't match, we can use router.replace to redirect the user to another page. We can also use router.events.on('routeChangeStart', callback) to listen for route changes and perform the same check before rendering the new page. Here is an example of how we can implement this logic in the App component:

import { useRouter } from 'next/router';
import { useEffect } from 'react';

function MyApp({ Component, pageProps }) {
  const router = useRouter();

  useEffect(() => {
    const handleRouteChange = (url) => {
      // Add your own logic here to check if the user is authenticated
      const userIsAuthenticated = checkUserAuthentication();

      if (!userIsAuthenticated && url === '/protected-page') {
        router.replace('/login');
      }
    };

    router.events.on('routeChangeStart', handleRouteChange);

    return () => {
      router.events.off('routeChangeStart', handleRouteChange);
    };
  }, []);

  return <Component {...pageProps} />;
}

export default MyApp;

We have implemented a simple way to protect routes in Next.js with the App component. You may need to customize it according to your needs and preferences. For example, you may want to use a different way to store and retrieve the user’s token, or you may want to use a different logic to determine the required status for each route. You may also want to handle some edge cases or errors.

Here’s the code:

import { useRouter } from &#x27;next/router&#x27;
import { useEffect } from &#x27;react&#x27;

function getUserStatus(token) {
  if (token === &#x27;admin&#x27;) {
    return &#x27;admin&#x27;
  } else if (token === &#x27;user&#x27;) {
    return &#x27;user&#x27;
  } else {
    return &#x27;guest&#x27;
  }
}

function getRequiredStatus(pathname) {
  if (pathname === &#x27;/admin&#x27;) {
    return &#x27;admin&#x27;
  } else if (pathname === &#x27;/profile&#x27;) {
    return &#x27;user&#x27;
  } else {
    return &#x27;guest&#x27;
  }
}

export default function App({ Component, pageProps }) {
  const router = useRouter()

  function checkAuth() {
    const token = localStorage.getItem(&#x27;token&#x27;)
    const userStatus = getUserStatus(token)
    const requiredStatus = getRequiredStatus(router.pathname)

    if (userStatus !== requiredStatus) {
      if (userStatus === &#x27;guest&#x27;) {
        router.replace(&#x27;/login&#x27;)
      } else {
        router.replace(&#x27;/error&#x27;)
      }
    }
  }

  useEffect(() => {
    checkAuth()
  }, [])

  useEffect(() => {
    const handleRouteChange = () => {
      checkAuth()
    }

    router.events.on(&#x27;routeChangeStart&#x27;, handleRouteChange)

    return () => {
      router.events.off(&#x27;routeChangeStart&#x27;, handleRouteChange)
    }
  }, [])

  return <Component {...pageProps} />
}

Using Middleware.js to Protect Routes in Next.js 13.4

Another way to protect routes in Next.js is to use middleware.js. Middleware.js allows you to run code before rendering a page on both the server and the client. You can use middleware.js to perform authentication, authorization, redirection, caching, or any other logic needed before rendering a page.

To use middleware.js, create a file called _middleware.js (or _middleware.ts if you are using TypeScript) in the pages directory and export a default function that receives a NextRequest object and returns a NextResponse object or nothing.

To protect routes with middleware.js, we can use a similar logic as we did with the App component. We can check the user’s status and compare it with the required status for the current route. If they don’t match, we can use NextResponse.redirect to redirect the user to another page.

Example code for middleware.js:

import { NextRequest, NextResponse } from &#x27;next/server&#x27;

function getUserStatus(token) {
  if (token === &#x27;admin&#x27;) {
    return &#x27;admin&#x27;
  } else if (token === &#x27;user&#x27;) {
    return &#x27;user&#x27;
  } else {
    return &#x27;guest&#x27;
  }
}

function getRequiredStatus(pathname) {
  if (pathname === &#x27;/admin&#x27;) {
    return &#x27;admin&#x27;
  } else if (pathname === &#x27;/profile&#x27;) {
    return &#x27;user&#x27;
  } else {
    return &#x27;guest&#x27;
  }
}

export default function middleware(req) {
  const token = req.cookies.token
  const userStatus = getUserStatus(token)
  const requiredStatus = getRequiredStatus(req.nextUrl.pathname)

  if (userStatus !== requiredStatus) {
    if (userStatus === &#x27;guest&#x27;) {
      return NextResponse.redirect(&#x27;/login&#x27;)
    } else {
      return NextResponse.redirect(&#x27;/error&#x27;)
    }
  }
}

This approach has advantages over using the App component, such as being faster, more flexible, and more secure. However, it also has some limitations, such as not being able to access React components or hooks or not being able to run on older browsers that don’t support ES modules. You can read more about middleware.js and its features and caveats in the official documentation.

Conclusion

The new App Router in Next.js provides a powerful and efficient way to handle routing and protect routes in your application. By using Middleware, exportMode, unstable_cache, and other new features, you can ensure that only authenticated users can access certain pages, components, and API endpoints. This not only enhances the security of your application but also provides a better user experience by directing users to the appropriate pages based on their authentication status.

Remember, the key to effectively using the App Router is understanding your application’s routing requirements and implementing the appropriate checks at each route. With a good understanding of the App Router and the examples in this guide, you should be well-equipped to protect routes in your Next.js application.

You can check out the official documentation for more information on the App Router in Next.js. Happy coding!