anila.

What is the difference between the Page and App router in Nextjs?

author avatar of anila website

Next.js offers two primary routing, systems: traditional Page Router and the modern App Router, the App Router is now the recommended approach for new projects. Why the shift? The App Router introduces powerful features like Server Components, which allow Next.js to render UI on the server, resulting in faster initial page loads and a more responsive user experience.

Additionally, the App Router provides a more structured way to organize the application using layouts and offers improved performance through advanced caching mechanisms. This article explores the core differences between these two routers, helping to choose the right one for buid next Next.js project.

File Structure & Routing

Both routers determine routing based on the project's file structure, but they do so in different ways.

Page Router

  • Uses the pages directory.
  • Each file within the pages directory automatically becomes a route based on its name and location. For example, pages/index.tsx corresponds to the and /pages/about.tsx becomes /about.
  • Every page is a Client Component by default.

Example:

src/
└── pages/
    ├── index.tsx
    ├── about.tsx
    └── contact/
        └── index.tsx

This translates to the following routes:

  • /
  • /about
  • /contact

Dynamic Routes

To create dynamic routes in the Page Router, use square brackets in the file name. For example, pages/posts/[id].tsx would create a dynamic route for individual posts.

  • The getStaticPaths function is used to specify which dynamic routes should be pre-rendered at build time.
  • The getStaticProps function fetches data for the specific dynamic route.

App Router

  • Uses the app directory.
  • Routing is defined by nested folders.
  • A special page.tsx file within each folder defines the route's content and makes it publicly accessible.
  • Every page is a Server Component by default.
  • Requires a root layout file (e.g., app/layout.tsx) that applies to all routes.

Example:

src/
└── app/
    ├── page.tsx
    ├── about/
    │   └── page.tsx
    └── contact/
    │   └── page.tsx
    └── layout.tsx

This also results in the routes:

  • /
  • /about
  • /contact

Dynamic Routes

Dynamic routes in the App Router are also created using square brackets in folder names (e.g., app/posts/[id]/page.tsx).

  • You can use the generateStaticParams function to pre-render dynamic routes at build time.

Key differences in file conventions:

FeatureApp RouterPage Router
Default ComponentServer ComponentClient Component
Directoryapppages
RoutingFolder-based routing with page.ts filesFile-based in pages
Dynamic Routes[param]/page.ts[param].ts
Root Layoutapp/layout.tsxpages/_app.tsx
404 Pageapp/not-found.tsxpages/404.tsx
Error Handlingapp/route.tsxpages/_error.tsx
API Routesapp/*/route.tsFiles in pages/api/*

API Routes

API Routes in Next.js are a feature that allows developers to create server-side endpoints directly within the application. These endpoints can handle HTTP requests, perform server-side logic, and respond with data, making it easier to build APIs without needing a separate backend service.

Page Router

  • Location: API routes in the Page Router are defined within the pages/api directory.
  • Routing Structure: Each file in this directory corresponds to a specific API endpoint. For example, the file pages/api/hello.ts would expose an API endpoint at /api/hello.
  • Request Handling: API routes in the Page Router utilize a single exported function to handle incoming requests. The function receives the req (request) and res (response) objects, which are similar to those found in Node.js's http module.
    • req provides information about the incoming request (method, headers, body).
    • res is used to send the response (data in JSON format, status codes, etc.).

Example (App Router Route Handler):

jsx

import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {

	if (req.method === 'GET') {
    // Fetch data from a database or external API
    try {
    const data = await fetchDataFromDatabase();
    res.status(200).json(data)
  } catch (err) {
    res.status(500).json({ error: 'Failed to fetch data' })
  }
  } else if (req.method === 'POST') {
    // Handle form submission or data creation
    const newData = req.body; // Access posted data
    await saveDataToDatabase(newData);
    res.status(201).json({ message: 'Data created successfully' });
  } else {
    // Handle any other HTTP methods
    res.setHeader('Allow', ['GET', 'POST']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }  
}

jsx

import { FormEvent } from 'react'

export default function Page() {
  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
 
    const formData = new FormData(event.currentTarget)
    const response = await fetch('/api/hello', {
      method: 'POST',
      body: formData,
    })
 
    // Handle response if necessary
    const data = await response.json()
    // ...
  }
 
  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  )
}

Usage

API routes in the Next.js Page Router are ideal for backend operations, suitable for scenarios where the API needs to be decoupled from the front end, allowing for easier management of backend logic, like simple CRUD operations, form submissions, integrations with third-party APIs, and webhook handling. They are easy to set up and integrate directly with Next.js applications, providing flexibility in handling requests independently from the React rendering process, making them particularly useful for straightforward API needs in applications.

Use Cases Example:

  1. Content Management: Manage blog posts or other content via CRUD operations.
  2. Simple CRUD Operations: Building RESTful APIs for Create, Read, Update, and Delete operations on resources.
  3. Form Handling and Data Submission: Collecting and processing form data submitted by users and validating it on the server
  4. Data Fetching with Authentication: Creating secure endpoints that require authentication.
  5. Integration with External APIs: Create a proxy to interact with third-party APIs securely.
  6. Webhooks: Handling incoming webhooks from third-party services like Stripe or GitHub.
  7. Data Validation: Validating data sent from the client before saving it in the database.
  8. User Authentication and Session Management: Creating endpoints to manage user login, logout, registration, token management, and session validation.
  9. File Upload Handling: Accepting file uploads from the client and saving them on the server or cloud storage.

App Router

Route Handlers are the evolution of API routes in the App Router, which aim to provide a more streamlined and integrated approach to API development within Next.js.

  • Location: API route handlers are defined in route.ts and located in the app directory, typically under a subdirectory named api. e.g., app/api/users/route.js creates the endpoint /api/users.
  • Routing Structure: The App Router supports nested routing, enabling more organized and modular code. For example, subdirectories within app/api can create hierarchical routes.
  • Request Handling: API route handlers in the App Router use named exports for each HTTP method, allowing for clearer and more structured code. which use the standard Request and Response objects from the Fetch API. This makes them more aligned with modern web standards and development practices.

Example:

jsx

// app/api/hello/route.ts
export async function GET(request: Request) {
 const { searchParams } = new URL(request.url);
 const id = searchParams.get('id')
 try {
    const response = await fetch(`https://data.com/${id}`)
    const result = response.json();
    return NextResponse.json(result);  
    
  } catch (err) {
    return NextResponse.json({ error });
  }
}

jsx

import { FormEvent } from 'react'

export default function Page() {
  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
 
    const formData = new FormData(event.currentTarget)
    const response = await fetch('/api/hello', {
      method: 'POST',
      body: formData,
    })
 
    // Handle response if necessary
    const data = await response.json()
    // ...
  }
 
  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  )
}

Usage:

  • Ideal for modern applications leveraging server components, where data fetching and UI rendering can be tightly integrated, enabling developers to build robust server-side functionalities that integrate smoothly with the overall application architecture. It provides flexibility and power of route handlers in managing data, handling user interactions, and connecting to external services while leveraging the benefits of modern React features.

Use Cases Example:

  1. Dynamic Data Fetching: Building APIs that return dynamic data based on incoming request parameters.
  2. Form Handling and Submission: Processing form submissions from client components and providing appropriate feedback.
  3. Integration with External APIs: Creating a proxy for external API calls, allowing for secure access and data manipulation before sending the response to the client.
  4. Authentication and Authorization: Implementing authentication mechanisms, such as logging in and managing sessions.
  5. Real-time Data Handling: Providing endpoints that facilitate real-time data updates, such as chat applications or live notifications.
  6. Custom Error Handling: Implementing custom error handling logic for specific types of requests or data validations.
  7. Batch Processing: Handling multiple requests or operations in a single API call, which is useful for optimizing network requests.

Key Differences:

FeatureAPI Routes (Page Router)Route Handlers (App Router)
Locationpages/apiapp/route.js
Mechanismreq, res objects (Node.js-like)Request, Response objects (Fetch API)
AdvantagesFamiliarity, simplicity for basic use casesModern approach, streamlined, integrated with Server Components, flexible routing, built-in caching
DisadvantagesLess integrated with React, more boilerplate, less flexible routingNewer concept, might require a learning curvences and project requirements.

Page Rendering & Data Fetching

Page Router

The Page Router offers flexible data fetching options:

Static Site Generation (SSG):

HTML is generated at build time. Ideal for content that doesn't change frequently. Can be used with or without data fetching. getStaticProps and getStaticPaths are key functions for SSG.

Example:

Let’s say we have a blog page that needs to display a list of blog posts fetched from an API, we would use getStaticProps to fetch the list of posts at the build time, and then pass them as props to the blog page component.

jsx

// posts will be populated at build time by getStaticProps()
export default function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}
 
// This function gets called at build time on server-side.
// It won't be called on client-side, so we can even do
// direct database queries.
export async function getStaticProps() {
  // Call an external API endpoint to get posts.
  // You can use any data fetching library
  const response = await fetch('https://.../posts')
  const posts = await response.json()
 
  // By returning { props: { posts } }, the Blog component
  // will receive `posts` as a prop at build time
  return {
    props: {
      posts,
    },
  }
}

During the build process, Next.js will call getStaticProps, fetch the list of posts from the API, and generate a pre-rendered HTML page with the blog post. This pre-recorded HTML page can then be served directly to the client, resulting in faster load times and better performance.

We might also want to pre render post/1, post/2 …etc. at build time, for this case, our page paths depend on external data so we would use getStaticPaths from a dynamic page (pages/posts/[id].js) to specify which paths we want to pre-render at the build time.

jsx

function Post({ post }) {
  // Render post...
}
 
export async function getStaticPaths() {
  const response = await fetch('https://.../posts')
  const posts = await response.json()
 
  // Get the paths we want to pre-render based on posts
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))
 
  // We'll pre-render only these paths at build time.
  return { paths }
}
 
// This also gets called at build time
export async function getStaticProps({ params }) {
  // params contains the post `id`.
  // If the route is like /posts/1, then params.id is 1
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()
 
  // Pass post data to the page via props
  return { props: { post } }
}
 
export default Post

Server-Side Rendering (SSR):

HTML is generated on each request. Use getServerSideProps to fetch data on the server. Ensures up-to-date data but can be less performant than SSG.

jsx

import type { InferGetServerSidePropsType, GetServerSideProps } from 'next'
 
export const getServerSideProps = (async () => {
  // Fetch data from external API
  const response = await fetch('https://.../data')
  const data = await response.json()
  // Pass data to the page via props
  return { props: { data } }
}) 

export default function Page({
  data,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
   // Render data...
}

Client-Side Rendering(CSR):

We can also use Client Side data fetching along with Static Site Generation or Server Side Rendering. For interactive components where data needs to be fetched after the initial page load, the browser downloads a minimal HTML page and JavaScript needed for the page, then JavaScript is downloaded, parsed, and executed to update the DOM and render the page. we can use useEffect or third party libraries like SWR to fetch data on the Client.

App Router

In the App Router, Next.js uses server side components by default, which allows processing and building parts of the UI components on the server and then sent to the user’s browser, we can also opt into using client components when needed.

Server Components:

By default, components in the App Router are Server Components that allow us to move data fetching to the server, closer to the data source, simplifying data handling. The result can be cached and reused on subsequent requests and across users. This can improve performance and reduce cost by reducing the amount of rendering and data fetching done on each request.

Example:

Fetching data on the server with the fetch API:

jsx

export default async function Page() {
  let data = await fetch('https://.../data')
  let posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Fetching data on the server with an ORM or database:

jsx

import { db, posts } from '@/lib/db'
 
export default async function Page() {
  let allPosts = await db.select().from(posts)
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Static Rendering (Default):

Static Rendering rendered route HTML at build time or in the background after data revalidation. The pre-render result is cached and can be served quickly to the client. The optimization allows us to share the result of rendering work between client and server requests. It’s great for pages where the content doesn’t change often and the result is the same for every user, like the static blog post.

Dynamic Rendering:

If a Server Component uses dynamic functions or fetches uncached data, Next.js switches to dynamic rendering, which generates the HTML on the server in real time for each user request (like navigating to a page). This is useful when a route HTML has data that is personalized or has information that is only known at request time, such as cookies or URL search parameters.

Switching to Dynamic Rendering

During rendering process, if a dynamic function or uncached data request is discovered, Next.js will switch to dynamically rendering the whole route. This table summarizes how dynamic functions and data caching affect whether a route is statically or dynamically rendered:

Dynamic FunctionsDataRoute
NoCachedStatically Rendered
YesCachedDynamically Rendered
NoNot CachedDynamically Rendered
YesNot CachedDynamically Rendered

Client Components:

As Nextjs uses the server component by default, we will need to add use client at the top of our client component to declare a boundary between the Server and the Client.

On the Server:

  • Next.js uses React to turn Server Components into a special format called the React Server Component Payload (RSC Payload). This payload contains references to the Client Components that will be used on the page.
  • Next.js then combines this RSC Payload with JavaScript instructions to generate the HTML for the route on the server.

On the Client:

  • The generated HTML is sent to the user's browser, allowing them to see a quick, non-interactive preview of the page immediately.
  • The RSC Payload helps connect the Client and Server Component trees, allowing for efficient updates to the DOM.
  • Finally, the JavaScript instructions are executed to "hydrate" the Client Components, making the user interface interactive.

Example:

jsx

// app/component/posts.js
'use client'

import { useState, useEffect } from 'react'

export function Posts() {
  const [posts, setPosts] = useState(null)
 
  useEffect(() => {
    async function fetchPosts() {
      let res = await fetch('https://api.vercel.app/blog')
      let data = await res.json()
      setPosts(data)
    }
    fetchPosts()
  }, [])
 
  if (!posts) return <div>Loading...</div>
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Conclusion

The Next.js App Router marks a significant shift in how developers can build web applications with the framework. By embracing Server Components, nested layouts, and a more flexible routing structure, it offers a modern approach that prioritizes performance and developer experience. While the learning curve might be steeper initially due to new concepts like Server Components and Route Handlers, the App Router's potential for improved performance, enhanced flexibility, and streamlined development makes it a compelling choice for new Next.js projects.

However, the Page Router remains a valuable tool, especially for simpler projects or when familiarity is paramount. By understanding the key differences, developers can choose the routing system that best aligns with their goals and embark on building high-quality Next.js applications.

contact
contact icon
contact iconcontact iconcontact iconcontact iconcontact icon

Feel free to follow me or reach out anytime! Open to work opportunities, collaborations, and connections.

Copyright © anila. All rights reserved.