28th Dec 2023

Migrate Next JS from Pages router to App router

next-js-model

What is Next.js?

Next JS is a full-fledged JavaScript UI framework that enables you to create superior, user-friendly static websites and web apps using React. It effectively handles the configuration and tooling related to React alongside offering necessary optimizations, features, and an architecture to support each development stage. Powered by React, Webpack, and Babel, this framework helps you develop intuitive products and solutions for all web browsers that run on Mac OS, Windows, Linux, iOS, and Android. It offers three fundamental rendering modes:

  • Client-side Rendering (CSR): Renders UI in the browser, providing a dynamic experience but potentially slower initial loads.
  • Server-Side Rendering (SSR): Generates HTML on the server, ensuring SEO-friendly content and fast initial load times.
  • Static Site Generation (SSG): Pre-renders entire pages at build time, resulting in blazing-fast performance and static asset-like scalability.

Routing Strategies

Next.js offers two distinct router types, each catering to different project needs and complexities.

1. Pages Router:

The traditional approach, ideal for simple applications with flat routing structures. If you're starting your Next.js adventure, the Page Router serves as your initial guide, mapping paths to components and rendering your UI. Let's unravel its workings and how you can leverage it effectively for building web applications.

Next-Js-overview-image

Example endpoint with Database operation:

                                
                                    
  import MaterialModel from "@/model/materialModel";
  import connectDB from "@/pages/lib/connectDb";
  import { Authentication } from "@/utils/authentication";
   
  const handler = async (req, res) => {
      if (req.method === "GET") {
          try {
              await connectDB();
              const materials = await MaterialModel.find({ stock: { $gt: 0 } });
              if (!materials) {
                  res.status(404).json({ success: false, message: "Matereials not found" })
              }
              res.status(200).json({ success: true, message: "Materials fetched", materials });
          }
          catch (err) {
              res.status(500).json({ success: false, message: "Server error on fetching materials" })
          }
      }
  }
   
  export default Authentication(handler);
  
                                
                            

The Core Concept:

Think of the Page Router as a conductor, orchestrating the relationship between URLs and React components. Each file within the pages directory gets automatically transformed into a route, offering an intuitive file-system-based approach. For example, a file named about.js in the pages directory translates to the route /about. This simplifies routing setup and promotes clean code organization.

Key Features:

  • Simplicity: Define routes by creating files within the pages directory, making navigation easier to understand and manage.
  • Nested Routes: While the Pages Router primarily supports flat structures, you can achieve nested routes using dynamic segments. For example, posts/[id].js allows creating routes like /posts/1 and /posts/2.
  • Data Fetching: Integrate data fetching methods like getStaticProps and getServerSideProps within your pages to retrieve data efficiently, optimizing performance and SEO.
  • File-Based Routing: The mapping of files to routes makes development intuitive and easier to understand, especially for newcomers to Next.js.

Pros:

  • Easy to learn: Perfect for beginners due to its intuitive file-system based approach.
  • Lightweight: Suitable for smaller projects or applications with simple routing needs.
  • Fast initial setup: Get started quickly with minimal configuration.

Cons:

  • Limited nesting: Complex applications with deep hierarchies might benefit from more advanced routing solutions.
  • Repetitive code: Large applications can lead to code duplication in multiple pages.
  • Scalability constraints: For extensive projects, the Pages Router might struggle with managing intricate routing structures.

When to Use the Page Router:

  • Small to medium-sized projects: Ideal for applications with straightforward routing and data fetching needs.
  • Prototyping and rapid development: Get your application off the ground quickly with minimal setup.
  • Learning Next.js: Provides a gentle introduction to routing concepts in Next.js.

The Page Router excels in these scenarios

2. App Router:

Next.js has long been a favourite for crafting interactive and performant web applications. While the traditional Pages Router served well for simpler projects, complex applications often demanded more. Enter the App Router, introduced in Next.js 13.4, providing a paradigm shift for architecting scalable and maintainable apps.

app-router-view

Example endpoint with Database operation:

                                
                                    
  import connectDB from "@/app/lib/connectDb";
  import MaterialModel from "@/model/materialModel";
  import { Authentication } from "@/utils/authentication";
  import { NextResponse } from "next/server";
  export const GET = Authentication(async (NextRequest) => {
      try {
          await connectDB();
          const materials = await MaterialModel.find({ stock: { $gt: 0 } });
          if (!materials) {
              return NextResponse.json({ success: false, message: "Materials not found" }, { status: 404 })
          }
          return NextResponse.json({ success: true, message: "Materials fetched", materials }, { status: 200 });
      }
      catch (err) {
          return NextResponse.json({ success: false, message: "Server error on fetching materials" }, { status: 500 })
      }
  })
  
                                
                            

Key Features:

  • Nested Routes: Organize your application logically with hierarchical URLs like /products/categories/:id. No more flat structures and spaghetti code!
  • Layouts: Maintain a consistent UI across pages with reusable layout components, streamlining development and design.
  • Server Components: Experience lightning-fast initial loads and enhanced SEO with components rendered directly on the server.
  • Data Fetching Flexibility: Implement various data fetching strategies (getServerSideProps, getStaticProps, getStaticPaths) seamlessly with Server Components.
  • Modern Rendering Options: Leverage features like incremental static regeneration (ISR) and streaming for dynamic and efficient content delivery.

Benefits:

  • Improved Organization: Nested routes and layouts foster structured applications, easier to navigate and maintain.
  • Performance Boost: Server Components and data fetching strategies optimize initial load times and SEO.
  • Developer Efficiency: Reusable layouts and streamlined data fetching reduce code duplication and development time.
  • Future-Proof Architecture: Supports evolving needs with modern rendering options and flexible data handling.

When to Use the App Router:

  • Larger, complex applications: With intricate structures and data requirements, the App Router shines.
  • Focus on performance and SEO: Enjoy the benefits of Server Components and efficient data fetching.
  • Modern development paradigms: Leverage layouts and nested routes for a cleaner and more maintainable codebase.

Why Migrate to the App Router?

app-pages-image

The App Router, introduced in Next.js 13.4, represents a significant leap forward in building complex and scalable web applications. While the Pages Router served its purpose for simpler projects, the App Router unlocks numerous advantages that justify serious consideration for a potential migration. Here's a detailed exploration of the compelling reasons to make the switch:

1. Structured and Scalable Architecture:

  • Nested Routes: Forget the limitations of flat structures. Organize your app logically with hierarchical paths like /products/categories/:id. This reflects real-world information hierarchies, improves navigation, and makes code easier to manage, especially as your application grows.
  • Nested Folders: Mirror your route structure with corresponding folders in the app directory. This physical mapping promotes intuitive project organization, fosters maintainability, and simplifies file discovery.

2. Consistent UI & Reduced Duplication:

  • Reusable Layouts: Craft a single layout component containing headers, footers, and global styles. This eliminates the need to repeat code across every page, ensuring UI consistency and reducing development time.
  • Component Composition: Build intricate UIs by composing smaller components within layouts, promoting code reusability and modularity. This makes maintaining complex interfaces significantly easier.

3.Performance Gains & Improved SEO:

  • Server Components: Experience lightning-fast initial loads with components rendered directly on the server. This dramatically improves perceived performance and user experience, especially for complex pages.
  • Data Fetching Flexibility: Implement various data fetching strategies (getServerSideProps, getStaticProps, getStaticPaths) seamlessly with Server Components. This allows you to fine-tune performance and data freshness based on your specific needs.
  • Automatic Code Splitting: The App Router automatically splits your application code based on routes, reducing initial bundle size and further improving loading times.

4. Modern Development Techniques:

  • Incremental Static Regeneration (ISR): Keep dynamic content fresh without full rebuilds. Ideal for content that changes occasionally but needs to stay up-to-date, like news feeds or blog posts.
  • Streaming: Efficiently deliver large datasets like videos or audio files without loading everything at once. This enhances the user experience for content-heavy applications.
  • Fine-Grained Control: Route Handlers (Route.js files) provide granular control over server-side routing and logic. This empowers developers to handle complex routing scenarios and leverage advanced features.

5. Integration with Third-Party Libraries:

  • React Router DOM: The familiar routing library from React integrates seamlessly with the App Router, allowing you to leverage existing knowledge and favourite features.
  • Other Compatible Libraries: Explore a growing ecosystem of compatible libraries to further enhance your development experience and functionalities.

Additional Considerations:

  • Learning Curve Migrating to the App Router involves understanding new concepts and patterns. Invest time in learning and experimenting to reap the full benefits.
  • Gradual Migration: Consider a phased approach, migrating components or sections piecemeal while maintaining compatibility with your existing setup.
  • Performance Monitoring: Track performance metrics after migration to identify potential bottlenecks and areas for further optimization.

Migrating from Pages router to App Router:

Next.js Version

To update to Next.js version 13, run the following command using your preferred package manager:

                                
                                    
  npm install next@latest react@latest react-dom@latest
 
                                
                            

ESLint Version

If you're using ESLint, you need to upgrade your ESLint version:

                                
                                    
  npm install -D eslint-config-next@latest
                                
                            

Step-by-step guide for migrating from Pages router to App router

upgrade-from-page-app

Moving to the App Router may be the first time using React features that Next.js builds on top of such as Server Components, Suspense, and more. When combined with new Next.js features such as special files and layouts, migration means new concepts, mental models, and behavioural changes to learn.

We recommend reducing the combined complexity of these updates by breaking down your migration into smaller steps. The app directory is intentionally designed to work simultaneously with the pages directory to allow for incremental page-by-page migration.

  • The app directory supports nested routes and layouts.
  • Use nested folders to define routes and a special page.js file to make a route segment publicly accessible.
  • Special file conventions are used to create UI for each route segment. The most common special files are page.js and layout.js.
  • Use page.js to define UI unique to a route.
  • Use layout.js to define UI that is shared across multiple routes.
  • .js, .jsx, or .tsx file extensions can be used for special files.
  • You can colocate other files inside the app directory such as components, styles, tests, and more.
  • Data fetching functions like getServerSideProps and getStaticProps have been replaced with a new API inside the app. getStaticPaths has been replaced with generateStaticParams.
  • pages/_app.js and pages/_document.js have been replaced with a single app/layout.js root layout.
  • pages/_error.js has been replaced with more granular error.js special files.
  • pages/404.js has been replaced with the not-found.js file.
  • pages/api/* API Routes have been replaced with the route.js (Route Handler) special file.

Step 1: Creating the app directory

Update to the latest Next.js version (requires 13.4 or greater):

                                
                                     
  npm install next@latest
                                
                            

st Next.js version (requires 13.4 or greater):

directory-image

Then, create a new app directory at the root of your project (or src/ directory).

Step 2: Creating a Root Layout

Create a new app/layout.tsx file inside the app directory. This is a root layout that will apply to all routes inside app.

                                
                                    
  export default function RootLayout({
    // Layouts must accept a children prop.
    // This will be populated with nested layouts or pages
    children,
  }: {
    children: React.ReactNode
  }) {
    return (
      <html lang="en">
        <body>{children}</body>
      </html>
    )
  }
                                
                            
  • The app directory must include a root layout.
  • The root layout must define html, and body tags since Next.js does not automatically create them.
  • The root layout replaces the pages/_app.tsx and pages/_document.tsx files.
  • .js, .jsx, or .tsx extensions can be used for layout files.

Migrating _document.js and _app.js:

If you have an existing _app.js or _document.js file, you can copy the contents (e.g. global styles) to the root layout (app/layout.js). Styles in app/layout.js will not apply to pages/*. You should keep _app/_document while migrating to prevent your pages/* routes from breaking. Once fully migrated, you can then safely delete them.

If you are using any React Context providers, they will need to be moved to a Client Component.

Migrating the getLayout() pattern to Layouts (Optional)

Next.js recommended adding a property to Page components to achieve per-page layouts in the pages directory. This pattern can be replaced with native support for nested layouts in the app directory.

Before:
                                
                                    
  export default function DashboardLayout({ children }) {
    return (
      <div>
        <h2>My Dashboard</h2>
        {children}
      </div>
    )
  }
                                
                            
                                
                                    
  import DashboardLayout from '../components/DashboardLayout'
  export default function Page() {
    return <p>My Page</p>
  }
  Page.getLayout = function getLayout(page) {
    return <DashboardLayout>{page}</DashboardLayout>
  }
  
                                
                            
After:

Remove the Page.getLayout property from pages/dashboard/index.js and follow the steps for migrating pages to the app directory.

                                
                                    
  export default function Page() {
    return <p>My Page</p>
  }
                                
                            

Move the contents of DashboardLayout into a new Client Component to retain pages directory behaviour.

                                
                                    
  'use client' // this directive should be at top of the file, before any imports.
  // This is a Client Component
  export default function DashboardLayout({ children }) {
    return (
      <div>
        <h2>My Dashboard</h2>
        {children}
      </div>
    )
  }
  
                                
                            

Import the DashboardLayout into a new layout.js file inside the app directory.

                                
                                    
  import DashboardLayout from './DashboardLayout'
  // This is a Server Component
  export default function Layout({ children }) {
    return <DashboardLayout>{children}</DashboardLayout>
  }
  
                                
                            

Step 3: Migrating next/head

In the pages directory, the next/head React component is used to manage head HTML elements such as title and meta. In the app directory, next/head is replaced with the new built-in SEO support.

Before:
                                
                                    
  import Head from 'next/head'
  export default function Page() {
    return (
      <>
        <Head>
          <title>My page title</title>
        </Head>
      </>
    )
    }
  
                                
                            
After:
                                
                                    
  import { Metadata } from 'next'
  export const metadata: Metadata = {
    title: 'My Page Title',
  }
  
  export default function Page() {
    return '...'
  }
  
                                
                            

Step 4: Migrating Pages

  • Pages in the app directory are Server Components by default. This is different from the pages directory where pages are Client Components.
  • Data fetching has changed in app. getServerSideProps, getStaticProps and getInitialProps have been replaced with a simpler API.
  • The app directory uses nested folders to define routes and a special page.js file to make a route segment publicly accessible.
migrate-pages-image

Step 5: Migrating Routing Hooks

A new router has been added to support the new behaviour in the app directory. In app, you should use the three new hooks imported from next/navigation: useRouter(), usePathname(), and useSearchParams().

  • The new useRouter hook is imported from next/navigation and has different behaviour to the useRouter hook in pages which is imported from next/router.
  • The useRouter hook imported from next/router is not supported in the app directory but can continue to be used in the pages directory.
  • The new useRouter does not return the pathname string. Use the separate usePathname hook instead.
  • The new useRouter does not return the query object. Use the separate useSearchParams hook instead.
  • You can use useSearchParams and usePathname together to listen to page changes. See the Router Events section for more details.
  • These new hooks are only supported in Client Components. They cannot be used in Server Components.
                                
                                    
  'use client'
  import { useRouter, usePathname, useSearchParams } from 'next/navigation'
  export default function ExampleClientComponent() {
    const router = useRouter()
    const pathname = usePathname()
    const searchParams = useSearchParams()
  
    // ...
  }
  
                                
                            

In addition, the new useRouter hook has the following changes:

  • isFallback has been removed because fallback has been replaced.
  • The locale, locales, defaultLocales, domainLocales values have been removed because built-in i18n Next.js features are no longer necessary in the app directory.
  • basePath has been removed. The alternative will not be part of useRouter. It has not yet been implemented.
  • asPath has been removed because the concept of as has been removed from the new router.
  • isReady has been removed because it is no longer necessary. During static rendering, any component that uses the useSearchParams() hook will skip the prerendering step and instead be rendered on the client at runtime.

Step 6: Migrating Data Fetching Methods

The pages directory uses getServerSideProps and getStaticProps to fetch data for pages. Inside the app directory, these previous data fetching functions are replaced with a simpler API built on top of fetch() and async React Server Components.

                                
                                    
  export default async function Page() {
    // This request should be cached until manually invalidated.
    // Similar to getStaticProps.
    // force-cache is the default and can be omitted.
    const staticData = await fetch(https://..., { cache: 'force-cache' })
    // This request should be refetched on every request.
    // Similar to getServerSideProps.
    const dynamicData = await fetch(https://..., { cache: 'no-store' })
    // This request should be cached with a lifetime of 10 seconds.
    // Similar to getStaticProps with the revalidate option.
    const revalidatedData = await fetch(https://..., {
      next: { revalidate: 10 },
    })
   
    return <div>...</div>
  }

  
                                
                            

By setting the cache option to no-store, we can indicate that the fetched data should never be cached. This is similar to getServerSideProps in the pages directory.

                                
                                    
  async function getProjects() {
    const res = await fetch(https://..., { cache: 'no-store' })
    const projects = await res.json()
    return projects
  }
   
  export default async function Dashboard() {
    const projects = await getProjects()
    return (
      <ul>
        {projects.map((project) => (
          <li key={project.id}>{project.name}</li>
        ))}
      </ul>
    )
  }
  
                                
                            

Accessing Request Object

In the pages directory, you can retrieve request-based data based on the Node.js HTTP API.

For example, you can retrieve the req object from getServerSideProps and use it to retrieve the request's cookies and headers.

                                
                                    

  // pages directory
  export async function getServerSideProps({ req, query }) {
    const authHeader = req.getHeaders()['authorization'];
    const theme = req.cookies['theme'];
    return { props: { ... }}
  }
  
  export default function Page(props) {
    return ...
  }
  
                                
                            

Dynamic paths (getStaticPaths)

In the pages directory, the getStaticPaths function is used to define the dynamic paths that should be pre-rendered at build time.

                                
                                    
  // pages directory
  import PostLayout from '@/components/post-layout'
  
  export async function getStaticPaths() {
    return {
      paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
    }
  }
  
  export async function getStaticProps({ params }) {
    const res = await fetch(https://.../posts/{params.id})
    const post = await res.json()
    return { props: { post } }
  }
  
  export default function Post({ post }) {
    return <PostLayout post={post} />
  }
    
                                
                            

In the app directory, getStaticPaths is replaced with generateStaticParams.

generateStaticParams behaves similarly to getStaticPaths, but has a simplified API for returning route parameters and can be used inside layouts. The return shape of

generateStaticParams is an array of segments instead of an array of nested param objects or a string of resolved paths.

                            
                                
                                    

  // app directory
  import PostLayout from '@/components/post-layout'
  
  export async function generateStaticParams() {
    return [{ id: '1' }, { id: '2' }]
  }
  
  async function getPost(params) {
    const res = await fetch(https://.../posts/{params.id})
    const post = await res.json()
    return post
  }
  
  export default async function Post({ params }) {
    const post = await getPost(params)
    return <PostLayout post={post} />
  }
  
                                
                            

Using the name generateStaticParams is more appropriate than getStaticPaths for the new model in the app directory. The get prefix is replaced with a more descriptive generate, which sits better alone now that getStaticProps and getServerSideProps are no longer necessary. The Paths suffix is replaced by Params, which is more appropriate for nested routing with multiple dynamic segments.

API Routes

API Routes continue to work in the pages/api directory without any changes. However, they have been replaced by Route Handlers in the app directory.

Route Handlers allow you to create custom request handlers for a given route using the Web Request and Response APIs.

                                
                                    
    export async function GET(request: Request) {}
  
                                
                            

Conclusion:

While not a mandatory upgrade for every project, the App Router empowers you to build robust, scalable, and future-proof applications. Its combination of nested routes, reusable layouts, Server Components, and modern rendering techniques unlocks significant performance gains, improved developer experience, and a foundation for sustainable growth.

If your project demands organization, performance, and embraces modern development practices, the App Router presents a compelling opportunity to elevate your Next.js application to the next level.

With this enhanced understanding of the "why" behind migration, you can make an informed decision for your project and embark on a journey to unlock the full potential of the App Router in building exceptional web experiences.

Let's develop your ideas into reality