hardFull Stack EngineerTechnology
How does Next.js App Router handle server-side rendering, static generation, and client-side navigation differently from the Pages Router?
Posted 18/04/2026
by Mehedy Hasan Ador
Question Details
Interviewer asked:
"We're migrating from Pages Router to App Router. Can you explain the fundamental differences in how pages are rendered and routed? What are the gotchas we should watch for?"
Suggested Solution
Rendering Strategies Comparison
Pages Router
// pages/products/[id].js
// Static: runs at build time
export async function getStaticProps({ params }) {
return { props: { product: await getProduct(params.id) } };
}
// Dynamic: runs on every request
export async function getServerSideProps({ params }) {
return { props: { product: await getProduct(params.id) } };
}
App Router
// app/products/[id]/page.tsx
// Default = Server Component = SSR
// No special exports needed!
async function ProductPage({ params }) {
const product = await getProduct(params.id);
return <ProductCard product={product} />;
}
// Static generation:
export const dynamic = 'force-static'; // or generateStaticParams
export async function generateStaticParams() {
const products = await getAllProducts();
return products.map(p => ({ id: p.id }));
}
// ISR:
export const revalidate = 3600; // revalidate every hour
Key Architectural Differences
| Feature | Pages Router | App Router |
|---|---|---|
| Default rendering | SSR (getServerSideProps) or SSG (getStaticProps) | RSC (streamed SSR) |
| Layouts | Per-page _app.js + _document.js | Nested layouts with layout.tsx |
| Data fetching | Special exports (getStaticProps, etc.) | Direct async/await in components |
| Client navigation | Full page JS loaded | Streaming + selective hydration |
| Loading states | Manual loading UI | Built-in loading.tsx |
| Error handling | _error.js | error.tsx per segment |
| API routes | pages/api/ | Route handlers in app/api/ |
Layout Persistence (Biggest UX Win)
// app/layout.tsx — persists across navigation
export default function RootLayout({ children }) {
return (
<html>
<body>
<Navbar /> {/* Doesn't re-render on navigation */}
{children}
</body>
</html>
);
}
// app/dashboard/layout.tsx — nested layout
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar /> {/* Persists within dashboard routes */}
{children}
</div>
);
}
Migration Gotchas
- "use client" boundary — Forgetting this causes "useState is not defined" errors
- No
getServerSideProps— Data fetching is now direct async in the component - Metadata API — Replace
<Head>withmetadataexport orgenerateMetadata - Route params — Now passed as props, not
useRouter().query - Middleware — Runs on Edge Runtime, not all Node.js APIs available