App Router vs Pages Router in Next.js

Next.js has two routing systems: Pages Router (pages/ directory) and App Router (app/ directory). App Router was introduced in Next.js 13 and became the recommended approach starting from version 13.4.

Key Differences

AspectPages RouterApp Router
Directorypages/app/
Default componentsClientServer (RSC)
Data fetchinggetServerSideProps, getStaticPropsasync components with fetch
Layouts_app.tsx, _document.tsxNested layout.tsx
Loading UIManual implementationloading.tsx out of the box
Errors_error.tsx globallyerror.tsx per route
StreamingNoYes, via Suspense
Server ActionsNoYes

Pages Router

In the Pages Router every file in the pages/ directory automatically becomes a route. Data is loaded through special functions:

// pages/problems/index.tsx
export async function getServerSideProps() {
  const res = await fetch('https://api.hackfrontend.com/problems')
  const problems = await res.json()
  return { props: { problems } }
}

export default function ProblemsPage({ problems }) {
  return (
    <ul>
      {problems.map(p => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  )
}

Layouts are implemented through _app.tsx which wraps all pages:

// pages/_app.tsx
export default function App({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  )
}

App Router

In the App Router components are server components by default. Data is loaded directly in the component:

// app/problems/page.tsx
import { db } from '@/lib/db'

export default async function ProblemsPage() {
  const problems = await db.problem.findMany()

  return (
    <ul>
      {problems.map(p => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  )
}

Layouts are nested and preserve state during navigation:

// app/problems/layout.tsx
export default function ProblemsLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <div className="problems-container">
      <Sidebar />
      <main>{children}</main>
    </div>
  )
}

What App Router Provides

Server Components by Default

Server component code does not end up in the client bundle. Heavy dependencies stay on the server:

// app/docs/[slug]/page.tsx
import { MDXRemote } from 'next-mdx-remote/rsc'
import { getDocBySlug } from '@/lib/docs'

export default async function DocPage({
  params
}: {
  params: { slug: string }
}) {
  const doc = await getDocBySlug(params.slug)
  return <MDXRemote source={doc.content} />
}

The next-mdx-remote library is not sent to the client. This is exactly how articles are rendered on Hack Frontend.

Streaming

App Router supports Streaming through loading.tsx files and React Suspense:

// app/dashboard/loading.tsx
export default function Loading() {
  return <DashboardSkeleton />
}

The user sees the skeleton immediately while data loads progressively.

Nested Layouts

Pages Router has a single global layout. In App Router layouts are nested and do not re-render when navigating between child pages:

When to Choose Which

App Router is suitable for new projects. It is the recommended approach and receives all new features.

Pages Router continues to be supported. If a project is already built on Pages Router and works well, migration is not required.

Interview tip:

Both routers can coexist in the same project. This allows gradual migration, one route at a time.

Useful Resources