Next.js Interview Prep
Next.js Fundamentals

App Router vs Pages Router

The Architectural Shift That Split Next.js in Two

LinkedIn Hook

Next.js has TWO routing systems — and using the wrong one in an interview can make you look 2 years behind.

The Pages Router (pages/) shipped in 2016. The App Router (app/) shipped in 2023. They look similar on the surface, but under the hood they are fundamentally different architectures.

Here's the kicker: most production apps still run Pages Router, but interviewers want you to know the App Router — because it's the future.

In Lesson 1.2, you'll learn exactly how both routers work, what changed and why, a side-by-side comparison table you can recite in interviews, and the migration traps that catch even senior developers.

Read the full lesson -> [link]

#NextJS #AppRouter #PagesRouter #ReactServerComponents #WebDevelopment #FrontendInterview #InterviewPrep


App Router vs Pages Router thumbnail


What You'll Learn

  • How file-based routing works in both pages/ and app/ — with side-by-side code
  • The real reason App Router exists (React Server Components, not just folder reorganization)
  • A comparison table covering layouts, data fetching, rendering, and more
  • Migration considerations and which router interviewers expect you to know

The Restaurant Analogy — Two Kitchens, One Restaurant

Imagine a restaurant that has been serving food for years with a kitchen in the back. The waiters (client) take orders, walk to the kitchen (server), grab the prepared dish, and bring it to the table. This is the Pages Router — it works, it's reliable, and millions of meals have been served this way.

Now the owner decides to renovate. They build a second kitchen right next to the dining area — a pass-through kitchen where chefs can hand dishes directly to diners without a waiter carrying them across the room. Some dishes still come from the back kitchen (client components), but most are now prepared and served right at the pass-through window (server components). This is the App Router.

The restaurant didn't tear down the old kitchen. Both kitchens can run at the same time. But new menu items are designed for the pass-through kitchen, and the owner recommends all new recipes use it.

That's Next.js today: two routing systems coexisting, but the App Router is the recommended path forward.


A Brief History — Why Two Routers Exist

Understanding the timeline helps you explain this confidently in interviews.

2016 — Pages Router ships with Next.js 1.0. Every file in pages/ becomes a route. pages/about.js maps to /about. Simple, elegant, revolutionary at the time.

2016-2022 — Pages Router evolves. getStaticProps, getServerSideProps, and getStaticPaths are added. API routes arrive in pages/api/. The router handles SSR, SSG, and ISR. It becomes the de facto standard for React SSR.

2022 — React Server Components (RSC) are announced. React itself introduces a new paradigm: components that run ONLY on the server and send zero JavaScript to the client. The Pages Router architecture cannot support this — it was designed around the assumption that every component eventually runs on the client.

2023 — Next.js 13.4 makes App Router stable. A completely new routing system built from the ground up to support RSC, nested layouts, streaming, and the new React architecture.

The key insight for interviews: The App Router isn't just a folder rename. It's a fundamentally different architecture designed around React Server Components. The Pages Router sends JavaScript for every component. The App Router sends JavaScript only for components that need interactivity.


File-Based Routing — Side by Side

Both routers use file-based routing, but the conventions are completely different.

Pages Router — The file IS the route

pages/
  index.js          ->  /
  about.js          ->  /about
  blog/
    index.js        ->  /blog
    [slug].js       ->  /blog/my-post
  api/
    users.js        ->  /api/users

In the Pages Router, the file name directly maps to the URL. The default export of each file is the page component:

// pages/about.js — Pages Router
import { useState } from "react";

export default function AboutPage() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>About Us</h1>
      <p>Clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

// Every component is a client component by default
// useState, useEffect, event handlers — all work everywhere
// This entire component's JS is shipped to the browser

App Router — The folder IS the route

app/
  page.js           ->  /
  layout.js         ->  Root layout (wraps everything)
  about/
    page.js         ->  /about
  blog/
    page.js         ->  /blog
    [slug]/
      page.js       ->  /blog/my-post
  api/
    users/
      route.js      ->  /api/users

In the App Router, folders define routes, and special files inside those folders define behavior. The page.js file is what makes a route publicly accessible:

// app/about/page.js — App Router
// This is a Server Component by default — no "use client" needed

export default function AboutPage() {
  // NO useState here — this runs on the server only
  // NO useEffect — there is no browser lifecycle
  // NO event handlers — there is no client-side JS for this component

  return (
    <div>
      <h1>About Us</h1>
      <p>This HTML is rendered on the server and sent as static HTML</p>
      <p>Zero JavaScript shipped to the browser for this component</p>
    </div>
  );
}

If you need interactivity, you explicitly opt in:

// app/about/counter.js — Client Component in App Router
"use client"; // This directive makes it a client component

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
// app/about/page.js — Composing server and client components
import Counter from "./counter"; // Client component imported into server component

export default function AboutPage() {
  return (
    <div>
      <h1>About Us</h1>
      <p>This paragraph is server-rendered — zero JS</p>
      <Counter /> {/* Only THIS part ships JavaScript to the browser */}
    </div>
  );
}

App Router vs Pages Router visual 1


Special Files — The App Router's Secret Weapon

The Pages Router has one convention: the default export is the page. The App Router introduces special files that control different aspects of a route segment:

FilePurposePages Router Equivalent
page.jsThe UI for a route (makes it publicly accessible)The file itself (about.js)
layout.jsShared UI that wraps child routes (persists across navigation)_app.js (but only one level)
loading.jsLoading UI shown while the page loads (Suspense boundary)Manual useState + spinner
error.jsError UI shown when something crashes (Error Boundary)Manual ErrorBoundary component
not-found.js404 UI for this route segmentpages/404.js (global only)
template.jsLike layout but re-mounts on every navigationNo equivalent
route.jsAPI endpoint (replaces pages/api)pages/api/endpoint.js

Layouts — The Biggest Architectural Win

The Pages Router has one global layout via _app.js. If you need different layouts for different sections (marketing site vs dashboard), you have to hack around it:

// pages/_app.js — Pages Router: ONE global layout
export default function App({ Component, pageProps }) {
  return (
    <GlobalLayout>
      <Component {...pageProps} />
    </GlobalLayout>
  );
}

// Want a different layout for /dashboard?
// You have to use per-page layouts — a workaround pattern:
DashboardPage.getLayout = function getLayout(page) {
  return <DashboardLayout>{page}</DashboardLayout>;
};

The App Router supports nested layouts natively. Each folder can have its own layout.js, and they nest automatically:

// app/layout.js — Root layout (required, wraps entire app)
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

// app/dashboard/layout.js — Dashboard layout (wraps all /dashboard/* routes)
export default function DashboardLayout({ children }) {
  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1">{children}</main>
    </div>
  );
}

// Visiting /dashboard/settings renders:
// RootLayout > DashboardLayout > SettingsPage
// The Sidebar NEVER re-renders when navigating between dashboard pages

Data Fetching — The Paradigm Shift

This is where the two routers differ most dramatically, and it is the most common interview question.

Pages Router — Special exported functions

// pages/blog/[slug].js — Pages Router data fetching

// Runs at BUILD time — SSG
export async function getStaticProps({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`);
  const data = await post.json();

  return {
    props: { post: data },      // Passed to the component as props
    revalidate: 60,             // ISR: regenerate every 60 seconds
  };
}

// Tells Next.js which dynamic paths to pre-render
export async function getStaticPaths() {
  const posts = await fetch("https://api.example.com/posts");
  const data = await posts.json();

  return {
    paths: data.map((post) => ({ params: { slug: post.slug } })),
    fallback: "blocking",
  };
}

// Runs at REQUEST time — SSR
export async function getServerSideProps({ req, res, params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`);
  const data = await post.json();

  return {
    props: { post: data },
  };
}

// The component receives data as props — it never fetches data itself
export default function BlogPost({ post }) {
  return <h1>{post.title}</h1>;
}

App Router — Fetch directly in the component

// app/blog/[slug]/page.js — App Router data fetching

// No special exported functions. The component IS async.
// Server Components can use async/await directly.

export default async function BlogPost({ params }) {
  const { slug } = await params;

  // fetch() is extended by Next.js with caching options
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 60 }, // ISR equivalent
  });
  const post = await res.json();

  return <h1>{post.title}</h1>;
}

// For SSG equivalent — tell Next.js which paths to pre-render
export async function generateStaticParams() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();

  return posts.map((post) => ({ slug: post.slug }));
}

The difference is not cosmetic. In the Pages Router, data fetching is separated from the component — the component receives data as props. In the App Router, data fetching is colocated with the component — the component fetches its own data. This means:

  • No prop drilling from page-level data fetching functions to deeply nested components
  • Each component can independently fetch exactly the data it needs
  • Next.js automatically deduplicates identical fetch requests (if two components fetch the same URL, only one request is made)

The Complete Comparison Table

This table is interview gold. Memorize the key rows.

FeaturePages Router (pages/)App Router (app/)
RoutingFile-based (file = route)Folder-based (folder + page.js = route)
Default component typeClient Component (all JS shipped)Server Component (zero JS shipped)
Layouts_app.js global + per-page hackNested layout.js per folder
Data fetchinggetStaticProps, getServerSideProps, getStaticPathsasync components, fetch() with cache options, generateStaticParams
Loading statesManual implementationloading.js (automatic Suspense boundary)
Error handlingManual Error Boundaryerror.js (automatic Error Boundary)
API routespages/api/endpoint.jsapp/api/endpoint/route.js
StreamingNot supportedBuilt-in with Suspense
Server ActionsNot supported"use server" for form handling and mutations
React Server ComponentsNot supportedDefault behavior
Metadatanext/head componentmetadata export or generateMetadata function
MiddlewareSupportedSupported (same)
StabilityMature, battle-tested (7+ years)Stable since Next.js 13.4 (2023)
Community resourcesMassive — most tutorials use Pages RouterGrowing rapidly

App Router vs Pages Router visual 2


Why the App Router Exists — React Server Components

This is the "why" that separates a strong interview answer from a mediocre one.

The App Router was not built because the Pages Router had bad folder structure. It was built because React itself changed.

React Server Components (RSC) introduced a new model:

  1. Server Components run on the server, render to HTML, and send zero JavaScript to the browser
  2. Client Components work like traditional React — they hydrate on the client and handle interactivity

The Pages Router cannot support this model. In the Pages Router, every component is a client component. Even if you use getServerSideProps to fetch data on the server, the component itself (with all its JavaScript) is still shipped to the browser for hydration.

The App Router was designed from scratch around this new reality:

  • Components are Server Components by default (zero JS)
  • You explicitly opt into client-side JavaScript with "use client"
  • The result: dramatically smaller bundles for content-heavy pages
Pages Router mental model:
  Server (data) -> Client (render + hydrate everything)

App Router mental model:
  Server (data + render most things) -> Client (hydrate only interactive parts)

Interview answer template: "The App Router exists because React introduced Server Components, which fundamentally change how rendering works. The Pages Router was built on the assumption that every component eventually runs on the client. The App Router was built on the assumption that most components should run on the server, and only interactive parts should ship JavaScript to the browser."


Migration Considerations

Interviewers love asking: "How would you migrate a Pages Router app to App Router?"

The Incremental Approach

Next.js supports running both routers simultaneously. This is critical to know:

my-app/
  app/              # App Router (new pages go here)
    dashboard/
      page.js
  pages/            # Pages Router (existing pages stay here)
    about.js
    blog/
      [slug].js

Rules when both coexist:

  • If the same route exists in both app/ and pages/, the App Router takes priority
  • Middleware works with both routers
  • You can migrate page by page — no big-bang rewrite required

What Changes During Migration

Pages Router PatternApp Router Equivalent
getStaticPropsfetch() with { next: { revalidate: N } } in an async component
getServerSidePropsfetch() with { cache: 'no-store' } in an async component
getStaticPathsgenerateStaticParams
useRouter() from next/routeruseRouter() from next/navigation (different API!)
next/head for metadataexport const metadata or generateMetadata
_app.jsapp/layout.js (root layout)
_document.jsapp/layout.js (the <html> and <body> tags)
pages/api/app/api/route.js or Server Actions

The Biggest Migration Trap

// WRONG — using next/router in App Router
import { useRouter } from "next/router"; // This import BREAKS in App Router

// CORRECT — using next/navigation in App Router
import { useRouter } from "next/navigation"; // Different module, different API

// The API is also different:
// Pages Router: router.query.slug
// App Router:   params.slug (passed as prop) or useParams()

Which Router Do Interviewers Ask About?

Short answer: Both, but they expect you to know the App Router deeply.

Typical interview scenarios:

  1. "Explain the difference between App Router and Pages Router" — The comparison table above is your answer
  2. "How would you fetch data in Next.js?" — Start with App Router (async components), then mention Pages Router equivalents
  3. "What are React Server Components?" — This is an App Router question. If you answer with getServerSideProps, you're showing Pages Router knowledge only
  4. "We have a legacy Pages Router app — how would you approach migration?" — Incremental migration, both routers coexisting, page-by-page strategy

Rule of thumb: Default to App Router in your answers. Mention Pages Router when comparing or when discussing migration. Never answer a modern Next.js question with only Pages Router patterns.


Common Mistakes

  • Confusing "Server Component" with "Server-Side Rendering" — SSR (via getServerSideProps) still ships JavaScript to the client for hydration. Server Components ship ZERO JavaScript. These are different concepts. The App Router uses Server Components by default; the Pages Router uses SSR as an option but every component is still a client component.

  • Importing from the wrong router modulenext/router is for Pages Router. next/navigation is for App Router. Using the wrong one causes runtime errors or silent bugs. In interviews, specifying the correct import signals that you have actually built with both systems.

  • Assuming App Router is "just a new folder structure" — The folder change from pages/ to app/ is the least important difference. The real shift is the rendering model: server-first components, nested layouts, streaming, and colocated data fetching. If you describe it as "they just moved files to app/", you are missing the entire architectural point.


Interview Questions

Q: What is the fundamental difference between the Pages Router and the App Router?

The Pages Router treats every component as a client component — all JavaScript is shipped to the browser for hydration. The App Router uses React Server Components by default, meaning most components run on the server and send zero JavaScript to the client. Only components marked with "use client" ship JavaScript. This is not just a routing change — it is a fundamental shift in how React components are rendered and delivered.

Q: Can you use both pages/ and app/ in the same Next.js project?

Yes. Next.js supports incremental adoption. Both routers can coexist in the same project. If the same route is defined in both, the App Router takes priority. This allows teams to migrate page by page without a full rewrite.

Q: How does data fetching differ between the two routers?

Q: What are the special files in the App Router and what do they do?

Q: Why can't you use useState or useEffect in a Server Component?

Server Components run on the server and produce static HTML. They have no browser lifecycle — there is no DOM, no mount/unmount cycle, no client-side state. useState manages client-side state that persists across re-renders in the browser. useEffect runs side effects after the component mounts in the browser. Neither concept applies to a component that only runs on the server. If you need these hooks, you must add the "use client" directive to make the component a Client Component.


Quick Reference -- Cheat Sheet

+------------------------------------------+-------------------------------------------+
|           PAGES ROUTER (pages/)          |            APP ROUTER (app/)              |
+------------------------------------------+-------------------------------------------+
| File = Route                             | Folder + page.js = Route                  |
| pages/about.js -> /about                 | app/about/page.js -> /about               |
+------------------------------------------+-------------------------------------------+
| All components are Client Components     | Components are Server Components           |
| (JS always shipped to browser)           | by default (zero JS shipped)              |
+------------------------------------------+-------------------------------------------+
| getStaticProps / getServerSideProps      | async component + fetch()                  |
| (data fetching separated from component) | (data fetching colocated in component)    |
+------------------------------------------+-------------------------------------------+
| _app.js (single global layout)           | layout.js (nested per-folder layouts)      |
+------------------------------------------+-------------------------------------------+
| import from 'next/router'                | import from 'next/navigation'              |
+------------------------------------------+-------------------------------------------+
| Manual loading/error states              | loading.js + error.js (automatic)          |
+------------------------------------------+-------------------------------------------+
| No streaming                             | Streaming with Suspense built-in           |
+------------------------------------------+-------------------------------------------+
| No Server Actions                        | "use server" for mutations                 |
+------------------------------------------+-------------------------------------------+

Interview Default: Answer with App Router. Mention Pages Router for comparison.

Previous: 01-why-nextjs-over-plain-react.md Next: 03-project-structure-and-conventions.md


This is Lesson 1.2 of the Next.js Interview Prep Course -- 8 chapters, 33 lessons.

On this page