Skip to main content

App Router vs. Pages Router

The routing system is the core of any modern web application. Throughout its development history, Next.js has substantially evolved its approach to mapping files into accessible browser URLs. The architectural transition from the traditional Pages Router (introduced in early Next.js versions) to the modern App Router (introduced in Next.js 13 and consolidated as the industry default) represents one of the most significant paradigm shifts in the React ecosystem.

In this document, we explore the conceptual and architectural differences between both models, analyze how files are organized on disk, and understand the transition across data fetching patterns.


1. Historical Evolution of Next.js Routers​

For more than six years, Next.js operated under the convention of the pages/ directory. This model popularized declarative routing in React: creating a file like pages/about.tsx was all it took for the framework to automatically generate the /about route.

Despite its initial simplicity, as enterprise-scale applications grew, the Pages Router exposed structural limitations:

  • Rigid Coupling Between Files and Public Routes: Any file placed inside pages/ (even auxiliary components or utilities) was treated as a public route, forcing developers to isolate UI components into external directories like src/components/.
  • Difficulty with Complex Nested Layouts: Preserving persistent navigation bars, side menus, or scroll state required overloading pages/_app.tsx or implementing unintuitive patterns via custom getLayout functions.
  • Fragmented Data Fetching: Retrieving data depended on special page-level functions (getServerSideProps, getStaticProps), which could not be executed inside individual child components.

To overcome these architectural hurdles and fully adopt the React Server Components (RSC) paradigm, the Next.js engineering team developed the App Router, hosted inside the app/ directory.


2. Structural Comparison: From Files to URLs​

The most immediate visible difference between both routers lies in how routes are resolved from the filesystem tree:

Directory structure comparison between Pages Router and App Router

Architectural Breakdown of the Diagram​

Left Panel: Pages Router (pages/)​

  • Core Rule: Every JavaScript or TypeScript file placed in the directory becomes an accessible public route.
  • pages/index.tsx maps to /.
  • pages/about.tsx maps directly to /about.
  • pages/blog/index.tsx serves /blog.
  • pages/blog/[slug].tsx captures dynamic URL parameters (/blog/:slug).
  • pages/_app.tsx wraps the entire application to inject global stylesheets and state providers, but re-evaluates across every page transition.

Right Panel: App Router (app/)​

  • Core Rule: Folders define URL route segments, and exclusively the special page.tsx file exposes that route to the public.
  • app/layout.tsx defines the root shell (<html> and <body>) shared across all child routes.
  • app/page.tsx defines the UI for the root route /.
  • app/about/page.tsx defines the /about route.
  • app/blog/page.tsx defines the /blog route.
  • app/blog/[slug]/page.tsx serves dynamic blog segments.
  • Safe Colocation: Unlike pages/, in the App Router you can safely store auxiliary components (button.tsx), scoped stylesheets (styles.module.css), unit tests (page.test.tsx), or helper utilities (utils.ts) inside the same about/ directory without risking accidental exposure as public URL endpoints.

3. Data Fetching Comparison​

Moving to the App Router changed not only file locations, but also fundamentally revamped the mental model for how data travels from servers to components:

Data Fetching Evolution: Monolithic in Pages Router vs Concurrent in App Router

Evolution Analysis​

  1. The Monolithic Approach of Pages Router (Left Panel):
    • All responsibility for querying databases or external APIs resided strictly inside the top-level getServerSideProps function on the page.
    • If a page required user profile data, course listings, and statistical metrics, it had to aggregate all queries into a single blocking promise. If one query stalled for 2 seconds, the entire page remained frozen, displaying nothing to the end user.
    • For deeply nested child components (such as UserCard) to receive data, parents had to manually forward props through intermediate levels (MainSection), an architectural friction known as Prop Drilling.
  2. The Colocated and Concurrent Approach of App Router (Right Panel):
    • The page (page.tsx) no longer acts as a heavy data intermediary, but rather as a lightweight visual orchestrator.
    • Each Server Component (UserHeader, CourseGrid, StatsWidget) autonomously executes its own async/await fetch logic or database query directly on the server (colocation).
    • Powered by React Suspense and Streaming, if an analytical widget takes longer to compute, wrapping it in <Suspense fallback={<Skeleton />}> allows the rest of the page to stream to the browser immediately, with the heavy widget streaming in progressively without blocking the screen.

Comparative Code: Data Fetching​

In Pages Router (Classic Approach)​

pages/cursos/[slug].tsx
import { GetServerSideProps } from 'next';

interface CourseProps {
course: { id: string; title: string; description: string };
}

// 1. The server function must be exported separately
export const getServerSideProps: GetServerSideProps = async (context) => {
const { slug } = context.params!;
const res = await fetch(`https://api.icesi.edu.co/courses/${slug}`);
const course = await res.json();

// 2. Injected into the component via mandatory props
return { props: { course } };
};

// 3. The React component receives pre-resolved props
export default function CourseDetail({ course }: CourseProps) {
return (
<article>
<h1>{course.title}</h1>
<p>{course.description}</p>
</article>
);
}

In App Router (Modern RSC Approach)​

In the App Router, every component inside app/ is by default a Server Component. It can be declared as an async function, allowing direct await calls inside its own body:

app/cursos/[slug]/page.tsx
interface PageProps {
params: Promise<{ slug: string }>;
}

// 1. The component is an async function running directly on the server
export default async function CourseDetail({ params }: PageProps) {
// In Next.js 15+, dynamic route parameters are resolved as a Promise
const { slug } = await params;

// 2. Direct data fetching inside the component body
const res = await fetch(`https://api.icesi.edu.co/courses/${slug}`);
const course = await res.json();

return (
<article>
<h1>{course.title}</h1>
<p>{course.description}</p>
</article>
);
}
Mental Simplification

You no longer need to memorize the nuanced differences between getServerSideProps, getStaticProps, and getInitialProps. In the App Router, you simply use the standard JavaScript fetch() API with granular caching configurations (cache: 'force-cache', cache: 'no-store', or { next: { revalidate: 60 } }).


4. Comprehensive Comparison Matrix​

FeaturePages Router (pages/)App Router (app/)
Root Directorypages/app/ (or src/app/)
Component ParadigmClient Components by default with full hydration.React Server Components (RSC) by default, explicit Client Components.
Route DefinitionFile-based (profile.tsx → /profile).Folder-based with page.tsx convention (profile/page.tsx).
File ColocationDifficult (any file in pages/ generates a public route).Native (safely store components and utilities inside each folder).
Nested LayoutsComplex; requires getLayout or a monolithic _app.tsx.Native and composable using per-segment layout.tsx files.
State PreservationPage transitions usually re-mount the entire component tree.Shared layouts preserve their state intact during navigations.
Error and Loading StatesManual state hooks (isLoading, error).Declarative special files (loading.tsx and error.tsx).
Metadata Definition (SEO)<Head> tag imported from next/head.Static metadata object or asynchronous generateMetadata function.
API EndpointsFiles in pages/api/*.ts with (req, res) handlers.Files named route.ts based on Web Fetch API standards (GET, POST).

5. Which Router Should You Choose for New Projects?​

The Vercel engineering team and the React core community designate the App Router as the recommended choice for all new web development. All recent React innovations—such as Server Components, streaming Suspense, Server Actions, and Turbopack compiler optimizations—are designed and prioritized specifically for the App Router architecture.

The Pages Router continues to be maintained for backward compatibility in legacy codebases, but does not receive new architectural features.


Self-Assessment Quiz​

Cargando cuestionario...