Seamless Routing: Bridging Next.js App Router and Pages Router for Shared Paths

next.js development

Learn how to manage shared URL patterns between Next.js App Router and Pages Router, enabling a smooth migration strategy while retaining legacy content using `next.config.mjs` fallback rewrites.

When migrating a Next.js application, developers often encounter a routing challenge: how to serve content from both the App Router and the Pages Router under the exact same URL pattern. This scenario typically arises when transitioning existing pages to the new App Router while needing to maintain an archive of older content built with the Pages Router, all accessible via a consistent URL structure like /issues/[issue_id].

The objective is to ensure that a request for a newly designed issue is handled by the App Router, while a request for a legacy archived issue is rendered by the Pages Router. For example, both https://nextjsweekly.com/issues/108 (new) and https://nextjsweekly.com/issues/90 (old) should appear on the same base path, yet dynamically use the appropriate router. Migrating extensive legacy content might not always be feasible due to differing data retrieval mechanisms, such as reliance on getStaticProps in the Pages Router versus server components in the App Router.

A common initial assumption is that Next.js might automatically fall back to the Pages Router if an App Router route for a specific ID doesn't exist. However, by default, when both App and Pages routers define identical routes (e.g., app/issues/[issue_id]/page.tsx and pages/issues/[issue_id].tsx), the App Router's route takes precedence. This behavior necessitates an alternative approach for conditional routing.

Initial Approach: Middleware with NextResponse.rewrite()

One method involves renaming the Pages Router's path and using Next.js middleware to conditionally rewrite URLs. This ensures the browser consistently displays the desired URL, while internally redirecting to the correct router.

For instance, the Pages Router route could be renamed from issues/[issue_id] to issues-page/[issue_id]. A proxy.ts (or middleware.ts) file would then inspect the incoming issue_id. Based on a defined threshold (e.g., LAST_PAGES_ROUTER_ISSUE_ID), the middleware decides whether to allow the request to proceed to the App Router or to rewrite it to the issues-page/ route for the Pages Router. The NextResponse.rewrite() method is crucial here, as it changes the internal path without altering the URL shown to the user.

// proxy.ts
const LAST_PAGES_ROUTER_ISSUE_ID = 107;

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const match = pathname.match(/^\/issues\/(\d+)$/);

  if (match) {
    const issueId = parseInt(match[1] ?? "0", 10);
    if (issueId <= LAST_PAGES_ROUTER_ISSUE_ID) {
      return NextResponse.rewrite(
        new URL(`\/issues-page\/${issueId}`, request.url)
      );
    }
  }
  return NextResponse.next();
}

While functional, this solution has a drawback: it relies on a hardcoded LAST_PAGES_ROUTER_ISSUE_ID and might be considered less elegant for scalable implementations.

Recommended Solution: next.config.mjs Fallback Rewrites

A more robust and cleaner solution leverages Next.js rewrite configuration within next.config.mjs, specifically the fallback rewrite option. This feature allows the application to attempt to serve a route, and if it doesn't exist, fall back to an alternative destination. This is ideal for incrementally migrating parts of an application while maintaining existing routes.

The configuration works as follows: Next.js first checks for the route within the App Router. If the path (/issues/:issue_id) does not resolve in the App Router, it then attempts to resolve it using the specified destination, which in this case points to the renamed Pages Router path (/issues-page/:issue_id). If found, the Pages Router content is rendered under the original App Router source path. If neither router can resolve the path, a 404 error is displayed.

// next.config.mjs
module.exports = {
  async rewrites() {
    return {
      fallback: [
        {
          source: "/issues/:issue_id",
          destination: `/issues-page/:issue_id`,
        },
      ],
    };
  },
};

This fallback rewrite approach is significantly more elegant, declarative, and scalable compared to the middleware-based solution, making it the preferred method for managing shared routes across Next.js App and Pages Routers.