{"id":124,"date":"2026-04-19T13:02:02","date_gmt":"2026-04-19T13:02:02","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/nextjs-app-router-vs-pages-router-practical-guide\/"},"modified":"2026-04-19T13:02:02","modified_gmt":"2026-04-19T13:02:02","slug":"nextjs-app-router-vs-pages-router-practical-guide","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/nextjs-app-router-vs-pages-router-practical-guide\/","title":{"rendered":"Next.js App Router vs Pages Router: what actually changes"},"content":{"rendered":"<p>I migrated a medium-sized Next.js app from Pages Router to App Router about four months ago. I had been putting it off because every article I read said something different. Some said the App Router was the future and you should migrate now. Others said it was half-baked and to wait. Both camps were kind of right.<\/p>\n<p>Now that I&rsquo;ve been living with it for a while: the App Router is better. But it&rsquo;s a genuinely different mental model, not just an upgrade path, and there are specific things you&rsquo;ll get wrong the first time.<\/p>\n<p>This is what I wish someone had told me before I started.<\/p>\n<h2 id=\"the-actual-difference-its-not-the-file-structure\">The actual difference \u2014 it&rsquo;s not the file structure<\/h2>\n<p>Most explanations of <code>nextjs app router<\/code> vs Pages Router lead with the file system changes: move from <code>\/pages\/<\/code> to <code>\/app\/<\/code>, use <code>page.tsx<\/code> files, add <code>layout.tsx<\/code> for shared layouts. That stuff matters, but it&rsquo;s not the conceptual shift.<\/p>\n<p>The real change is that App Router is built around React Server Components. By default, every component in <code>\/app\/<\/code> is a Server Component \u2014 it renders on the server, sends HTML to the client, and ships no JavaScript for that component to the browser.<\/p>\n<p>If you want client-side interactivity, you opt in explicitly with <code>'use client'<\/code> at the top of the file.<\/p>\n<p>Pages Router is the opposite: every page is a client component by default. You use <code>getServerSideProps<\/code> or <code>getStaticProps<\/code> to pull in server-side data, but the component itself runs in the browser.<\/p>\n<p>Here&rsquo;s how that looks in practice:<\/p>\n<pre><code class=\"language-jsx\">\/\/ pages\/dashboard.tsx \u2014 Pages Router (client component by default)\nimport { useState } from 'react'\n\nexport default function Dashboard({ initialData }) {\n  const [data, setData] = useState(initialData)\n  return &lt;div&gt;{data.title}&lt;\/div&gt;\n}\n\nexport async function getServerSideProps() {\n  const data = await fetchDashboardData()\n  return { props: { initialData: data } }\n}\n<\/code><\/pre>\n<pre><code class=\"language-jsx\">\/\/ app\/dashboard\/page.tsx \u2014 App Router (server component by default)\nasync function Dashboard() {\n  const data = await fetchDashboardData() \/\/ direct async call, no special API\n  return &lt;div&gt;{data.title}&lt;\/div&gt;\n}\n<\/code><\/pre>\n<p>The App Router version is shorter. But if you need state or user interaction, you split the component:<\/p>\n<pre><code class=\"language-jsx\">\/\/ app\/dashboard\/page.tsx\nimport DashboardClient from '.\/DashboardClient'\n\nasync function Dashboard() {\n  const data = await fetchDashboardData()\n  return &lt;DashboardClient initialData={data} \/&gt;\n}\n\n\/\/ app\/dashboard\/DashboardClient.tsx\n'use client'\nimport { useState } from 'react'\n\nexport default function DashboardClient({ initialData }) {\n  const [data, setData] = useState(initialData)\n  return &lt;div&gt;{data.title}&lt;\/div&gt;\n}\n<\/code><\/pre>\n<p>That split is the thing you&rsquo;ll fight with for the first few weeks.<\/p>\n<h2 id=\"data-fetching-where-it-got-confusing-for-me\">Data fetching: where it got confusing for me<\/h2>\n<p>In Pages Router, data fetching is explicit and centralized. <code>getServerSideProps<\/code> for dynamic data, <code>getStaticProps<\/code> for build-time data. One function, one place per page.<\/p>\n<p>In App Router, any Server Component can just <code>await<\/code> a database call, an API call, whatever. No special API needed \u2014 it&rsquo;s just async functions.<\/p>\n<p>This is genuinely nice once you&rsquo;re used to it. You stop thinking about &ldquo;where do I put the data fetching&rdquo; and just fetch where you need it. The downside is that data fetching can get scattered across the component tree, which takes discipline to keep organized.<\/p>\n<p>The caching model also changed significantly. Pages Router has a relatively simple story: SSR means fresh data every request, SSG means build-time data. App Router has a granular caching system where you control caching at the individual fetch level:<\/p>\n<pre><code class=\"language-tsx\">\/\/ Revalidate every hour\nconst data = await fetch('https:\/\/api.example.com\/data', {\n  next: { revalidate: 3600 }\n})\n\n\/\/ No caching \u2014 always fresh\nconst data = await fetch('https:\/\/api.example.com\/data', {\n  cache: 'no-store'\n})\n<\/code><\/pre>\n<p>More powerful than Pages Router. Also more footguns. The <a href=\"https:\/\/nextjs.org\/docs\/app\/building-your-application\/caching\" rel=\"nofollow noopener\" target=\"_blank\">Next.js caching documentation<\/a> is worth reading before you ship anything to production \u2014 the default behavior isn&rsquo;t always what you&rsquo;d expect.<\/p>\n<h2 id=\"the-mistake-everyone-makes-first\">The mistake everyone makes first<\/h2>\n<p>Based on my own migration and talking to others who&rsquo;ve done it: the most common first mistake is putting too much in Client Components.<\/p>\n<p>The pattern goes like this: you move a page to App Router, something breaks, you add <code>'use client'<\/code> at the top of the file, it works. You repeat this until everything works. By the end, your app is functionally equivalent to Pages Router \u2014 you&rsquo;ve just moved files around.<\/p>\n<p>That&rsquo;s not fatal, but you&rsquo;ve missed the performance benefits and the architectural point.<\/p>\n<p>The right approach is pushing <code>'use client'<\/code> as far down the component tree as possible. Keep it on the interactive leaves \u2014 buttons, forms, inputs \u2014 and let everything above them stay as Server Components. This takes restructuring. The payoff is real: less JavaScript shipped to the client, faster initial loads, and a cleaner separation between data fetching and UI state.<\/p>\n<p>The second thing that catches people is passing non-serializable values from Server Components to Client Components. You can&rsquo;t pass a function or a class instance as a prop across that boundary. Strings, numbers, plain objects \u2014 fine. Functions and complex objects \u2014 you&rsquo;ll get an error. Restructuring around this is annoying but usually points you toward a cleaner design anyway.<\/p>\n<p>If you&rsquo;re migrating and want to understand how this affects form handling and mutations, I covered how Server Actions changed the way I think about API routes in a separate post \u2014 <a href=\"https:\/\/abrarqasim.com\/blog\/nextjs-server-actions-stopped-writing-api-routes\" rel=\"noopener\">why I stopped writing API routes after Server Actions<\/a>. The two changes are related and fit together once you see them together.<\/p>\n<h2 id=\"should-you-migrate-an-existing-project\">Should you migrate an existing project?<\/h2>\n<p>If your project is actively developed and you&rsquo;re planning to stay on Next.js long-term: yes. The App Router is where Next.js is going. Pages Router is in maintenance mode \u2014 it gets security fixes, not new features.<\/p>\n<p>The migration itself isn&rsquo;t that painful, but it isn&rsquo;t automatic either. The Next.js team has an <a href=\"https:\/\/nextjs.org\/docs\/app\/building-your-application\/upgrading\/app-router-migration\" rel=\"nofollow noopener\" target=\"_blank\">incremental migration guide<\/a> that lets you run both routers simultaneously \u2014 <code>\/pages<\/code> and <code>\/app<\/code> coexist in the same project. I migrated one route at a time over about two weeks.<\/p>\n<p>The parts that take the longest aren&rsquo;t the routing changes \u2014 they&rsquo;re the data fetching restructuring and the client\/server splits. Budget more time for those than you think you need.<\/p>\n<p>If your project is small and not actively changing: probably not worth the disruption. Pages Router isn&rsquo;t going away; it&rsquo;s just not getting new features.<\/p>\n<h2 id=\"the-mental-model-click\">The mental model click<\/h2>\n<p>App Router makes sense all at once, not gradually. The first two weeks feel like working against the framework. Then something shifts and it starts feeling natural.<\/p>\n<p>The click usually happens when you stop treating &ldquo;where do I fetch this data&rdquo; and &ldquo;is this component client or server&rdquo; as two separate decisions and start thinking about them together. Server by default. Client when you need interactivity. Fetch at the component that needs the data, not at a centralized function at the top of the page.<\/p>\n<p>It&rsquo;s a better model. Getting there takes some patience.<\/p>\n<p>I write about this kind of stack transition stuff at <a href=\"https:\/\/abrarqasim.com\" rel=\"noopener\">abrarqasim.com<\/a> \u2014 come by if you&rsquo;re navigating similar decisions.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The Next.js App Router and Pages Router aren&#8217;t just different file structures \u2014 they&#8217;re different mental models. Here&#8217;s what changes and what trips you up first.<\/p>\n","protected":false},"author":2,"featured_media":123,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"The Next.js App Router and Pages Router aren't just different file structures \u2014 they're different mental models. Here's what changes and what trips you up first.","rank_math_focus_keyword":"nextjs app router","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[35],"tags":[99,61,41,39],"class_list":["post-124","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-web-development","tag-app-router","tag-nextjs","tag-react","tag-web-development"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/124","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=124"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/124\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/123"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=124"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=124"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=124"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}