Skip to content
C Codeloom
Web

Next.js App Router vs Pages Router

Compare the Next.js App Router and Pages Router: routing, data fetching, layouts, server components, and how to decide for new and existing projects.

·5 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How routing differs between the two
  • Where data fetching lives in each
  • Layouts, templates, and nested routes
  • Server Components in App Router
  • When to migrate vs stay

Prerequisites

  • Comfortable with React

Next.js currently ships two routers in the same framework: the older Pages Router and the newer App Router. Both are supported, both can live in the same project, and the choice between them shapes how you fetch data, share layouts, and structure your code. This post compares the two without taking sides, so you can decide based on your project’s reality.

What and why

The Pages Router (the pages/ directory) is the original Next.js model. Each file is a route. Data is fetched with getServerSideProps, getStaticProps, or getStaticPaths. Everything in pages/ is a client component by React’s old definitions; SSR produces HTML, then the client hydrates.

The App Router (the app/ directory) is a newer model built around React Server Components. Routing is folder-based with conventions: page.tsx is a route, layout.tsx is a nested layout, loading.tsx is a Suspense fallback, error.tsx is an error boundary. Data fetching happens inside async Server Components by await-ing directly.

Both target the same goals: SSR, SSG, ISR, client-side navigation. The App Router pushes more work to the server by default.

Mental model

The Pages Router is “one file, one route, one data function.” The App Router is “a folder tree where each folder can contribute layout, loading, errors, and a page.”

Pages Router:
pages/
  index.tsx
  blog/
    [slug].tsx          // dynamic route
    index.tsx
api/
  hello.ts              // API route

App Router:
app/
  layout.tsx            // root layout (Server Component)
  page.tsx              // home
  blog/
    layout.tsx          // blog-specific layout
    page.tsx            // /blog
    loading.tsx         // Suspense fallback
    error.tsx           // Error boundary
    [slug]/
      page.tsx          // /blog/:slug
app/api/
  hello/route.ts        // route handler
Folder structures

Hands-on example

Pages Router data fetching:

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

export default function Post({ post }) {
  return <article><h1>{post.title}</h1><p>{post.body}</p></article>;
}

export const getServerSideProps: GetServerSideProps = async ({ params }) => {
  const res = await fetch(`https://api.example.com/posts/${params!.slug}`);
  const post = await res.json();
  return { props: { post } };
};

App Router equivalent:

// app/blog/[slug]/page.tsx
export default async function Post({ params }: { params: { slug: string } }) {
  const res = await fetch(`https://api.example.com/posts/${params.slug}`, {
    next: { revalidate: 60 },
  });
  const post = await res.json();
  return <article><h1>{post.title}</h1><p>{post.body}</p></article>;
}

The App Router version is a Server Component. It awaits the fetch directly. No getServerSideProps. The revalidate option puts ISR-like caching control inline with the fetch.

Layouts are persistent across navigation in the App Router:

// app/blog/layout.tsx
export default function BlogLayout({ children }) {
  return (
    <div>
      <aside>Blog sidebar</aside>
      <main>{children}</main>
    </div>
  );
}

The sidebar mounts once and stays mounted as you navigate between /blog/post-a and /blog/post-b. With the Pages Router, layouts traditionally required _app.tsx plus per-page composition.

Server Actions and mutations

In the App Router, you can mutate data from a form by marking a function with "use server":

async function createPost(formData: FormData) {
  'use server';
  await db.posts.create({ data: { title: formData.get('title') as string } });
  revalidatePath('/blog');
}

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">Create</button>
    </form>
  );
}

There is no API route for this case. The Pages Router would require pages/api/posts.ts plus a client fetch.

When to use which

Choose the App Router for new projects that benefit from Server Components: data-heavy dashboards, content sites, marketing pages with personalization. The smaller client bundle and built-in streaming are real wins.

Stick with or migrate gradually to the Pages Router only if you have a large existing codebase, a dependency that does not play well with Server Components (some client-only libraries), or a team unfamiliar with the new model.

You can run both in the same Next.js app. Pages in pages/ take priority for matching URLs.

Common pitfalls

  • Marking everything as "use client" in the App Router. You lose the bundle benefits and end up with a glorified Pages Router. Push the directive as deep as possible.
  • Trying to use getServerSideProps inside app/. It does not exist there. Data fetching moves into the component itself.
  • Forgetting that params and searchParams in the App Router can be promises (in recent versions). Await them.
  • Mixing next/router and next/navigation. The first is for Pages Router, the second for App Router. They are not interchangeable.

Best practices

  • Default to Server Components in the App Router and opt into client only where you need state, effects, or browser APIs.
  • Use route handlers (route.ts) instead of API routes in the App Router.
  • Leverage loading.tsx and error.tsx instead of writing them by hand. They map to Suspense and Error Boundaries automatically.
  • For migrations, move routes a folder at a time. The two routers coexist gracefully.

FAQ

Is the Pages Router deprecated? Not officially. Next.js continues to support it, but new framework features land in the App Router first.

Can I use Server Components in the Pages Router? No. RSC support is App Router only.

Which is faster? Both can be very fast. The App Router has an edge for data-heavy pages because of smaller client bundles and streaming.

What about Turbopack and the rest? Both routers benefit from Turbopack dev builds. The runtime characteristics depend on the router model, not the bundler.