What is the difference between the Page and App router in Nextjs?
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.
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.
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:
Feature
App Router
Page Router
Default Component
Server Component
Client Component
Directory
app
pages
Routing
Folder-based routing with page.ts files
File-based in pages
Dynamic Routes
[param]/page.ts
[param].ts
Root Layout
app/layout.tsx
pages/_app.tsx
404 Page
app/not-found.tsx
pages/404.tsx
Error Handling
app/route.tsx
pages/_error.tsx
API Routes
app/*/route.ts
Files 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'exportdefaultasyncfunctionhandler(req: NextApiRequest,res: NextApiResponse){if(req.method ==='GET'){// Fetch data from a database or external APItry{const data =awaitfetchDataFromDatabase(); res.status(200).json(data)}catch(err){ res.status(500).json({error:'Failed to fetch data'})}}elseif(req.method ==='POST'){// Handle form submission or data creationconst newData = req.body;// Access posted dataawaitsaveDataToDatabase(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`);}}
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:
Content Management: Manage blog posts or other content via CRUD operations.
Simple CRUD Operations: Building RESTful APIs for Create, Read, Update, and Delete operations on resources.
Form Handling and Data Submission: Collecting and processing form data submitted by users and validating it on the server
Data Fetching with Authentication: Creating secure endpoints that require authentication.
Integration with External APIs: Create a proxy to interact with third-party APIs securely.
Webhooks: Handling incoming webhooks from third-party services like Stripe or GitHub.
Data Validation: Validating data sent from the client before saving it in the database.
User Authentication and Session Management: Creating endpoints to manage user login, logout, registration, token management, and session validation.
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.tsexportasyncfunctionGET(request: Request){const{ searchParams }=newURL(request.url);const id = searchParams.get('id')try{const response =awaitfetch(`https://data.com/${id}`)const result = response.json();return NextResponse.json(result);}catch(err){return NextResponse.json({ error });}}
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:
Dynamic Data Fetching: Building APIs that return dynamic data based on incoming request parameters.
Form Handling and Submission: Processing form submissions from client components and providing appropriate feedback.
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.
Authentication and Authorization: Implementing authentication mechanisms, such as logging in and managing sessions.
Real-time Data Handling: Providing endpoints that facilitate real-time data updates, such as chat applications or live notifications.
Custom Error Handling: Implementing custom error handling logic for specific types of requests or data validations.
Batch Processing: Handling multiple requests or operations in a single API call, which is useful for optimizing network requests.
Key Differences:
Feature
API Routes (Page Router)
Route Handlers (App Router)
Location
pages/api
app/route.js
Mechanism
req, res objects (Node.js-like)
Request, Response objects (Fetch API)
Advantages
Familiarity, simplicity for basic use cases
Modern approach, streamlined, integrated with Server Components, flexible routing, built-in caching
Disadvantages
Less integrated with React, more boilerplate, less flexible routing
Newer 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()exportdefaultfunctionBlog({ 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.exportasyncfunctiongetStaticProps(){// Call an external API endpoint to get posts.// You can use any data fetching libraryconst response =awaitfetch('https://.../posts')const posts =await response.json()// By returning { props: { posts } }, the Blog component// will receive `posts` as a prop at build timereturn{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
functionPost({ post }){// Render post...}exportasyncfunctiongetStaticPaths(){const response =awaitfetch('https://.../posts')const posts =await response.json()// Get the paths we want to pre-render based on postsconst 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 timeexportasyncfunctiongetStaticProps({ params }){// params contains the post `id`.// If the route is like /posts/1, then params.id is 1const res =awaitfetch(`https://.../posts/${params.id}`)const post =await res.json()// Pass post data to the page via propsreturn{props:{ post }}}exportdefault 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'exportconst getServerSideProps =(async()=>{// Fetch data from external APIconst response =awaitfetch('https://.../data')const data =await response.json()// Pass data to the page via propsreturn{props:{ data }}})exportdefaultfunctionPage({ data,}: InferGetServerSidePropsType<typeofgetServerSideProps>) {// 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
exportdefaultasyncfunctionPage(){let data =awaitfetch('https://.../data')let posts =await data.json()return(<ul>{posts.map((post)=>(<likey={post.id}>{post.title}</li>))}</ul>)}
Fetching data on the server with an ORM or database:
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 Functions
Data
Route
No
Cached
Statically Rendered
Yes
Cached
Dynamically Rendered
No
Not Cached
Dynamically Rendered
Yes
Not Cached
Dynamically 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'exportfunctionPosts(){const[posts, setPosts]=useState(null)useEffect(()=>{asyncfunctionfetchPosts(){let res =awaitfetch('https://api.vercel.app/blog')let data =await res.json()setPosts(data)}fetchPosts()},[])if(!posts)return<div>Loading...</div>return(<ul>{posts.map((post)=>(<likey={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.