Topics
v5.0.0
On this page · sections

App Router & Routing

Next.js ships two routing systems. App Router (app/) is server-first with React Server Components; Pages Router (pages/) is the legacy client-centric model. Both can coexist during migration.

AspectPages RouterApp Router
Default component typeClientServer Component
Data fetchinggetStaticProps / getServerSideProps exportsAsync Server Components + fetch
Layouts_app.tsx remounts; getLayout patternNested layout.tsx persists on nav
Loading / error UIManual Suspense / error boundariesloading.tsx, error.tsx conventions
RecommendationMaintained, legacyDefault for new work (Next.js 13+)

App Router file conventions

Definition

The App Router uses file-system conventions: page.tsx, layout.tsx, loading.tsx, error.tsx, and route.ts for API endpoints.

// app/dashboard/layout.tsx export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { return ( <div className="dashboard"> <nav>Dashboard nav — persists on child nav</nav> {children} </div> ); } // app/dashboard/settings/page.tsx export default function SettingsPage() { return <h1>Settings</h1>; }

Route tree:

app/layout.tsx └── dashboard/layout.tsx ← persists on /dashboard/* nav └── settings/page.tsx

Interview: App Router components are Server Components by default. Add 'use client' only when you need hooks, event handlers, or browser APIs.

Pages Router architecture

Definition

Pages Router uses pages/ with _app.tsx and _document.tsx wrappers. Data fetching lives in separate exports on each page.

// pages/dashboard/settings.tsx export async function getServerSideProps() { const session = await getSession(); return { props: { user: session.user } }; } export default function SettingsPage({ user }: { user: User }) { return <h1>Settings for {user.name}</h1>; }

_app.tsx wraps every page and remounts on navigation unless you use a custom getLayout pattern.

Interview: Pages Router separates data fetching from UI. App Router colocates fetch with the component that renders it.

Nested layouts and templates

Definition

layout.tsx wraps child segments and does not remount on navigation. State in layouts survives route changes within the segment.

// app/shop/layout.tsx export default function ShopLayout({ children }: { children: React.ReactNode }) { return ( <div className="shop"> <aside>Filters — state persists</aside> <main>{children}</main> </div> ); }

template.tsx remounts on navigation — use for enter animations where you need a fresh mount.

On navigation from /shop/a to /shop/b:

1. Layout persists — sidebar state survives 2. loading.tsx shows immediately (if present) 3. page.tsx async fetch runs 4. Content swaps in — layout unchanged

Interview: Layouts are shared shells. Templates remount — layouts do not.

Dynamic routes

Definition

Bracket folders create dynamic segments. Catch-all and optional catch-all cover nested paths.

app/blog/[slug]/page.tsx → /blog/hello-world app/docs/[...slug]/page.tsx → /docs/a/b/c (catch-all) app/shop/[[...slug]]/page.tsx → /shop or /shop/a/b (optional)
// app/blog/[slug]/page.tsx export default async function PostPage(props: { params: Promise<{ slug: string }>; }) { const { slug } = await props.params; const post = await fetchPost(slug); return <article>{post.title}</article>; }

Prebuild known paths with generateStaticParams:

export async function generateStaticParams() { const slugs = await fetchAllSlugs(); return slugs.map((slug) => ({ slug })); } export const dynamicParams = true; // on-demand ISR for unknown slugs

Real-world example

Docs site with thousands of pages: prebuild top 100, generate the rest on first visit.

Interview: params is a Promise in Next.js 15+. Await it before use.

Route groups

Definition

Route groups (folder) organize layouts without affecting the URL:

app/ ├── (marketing)/ │ ├── layout.tsx ← marketing chrome │ └── page.tsx → / ├── (shop)/ │ ├── layout.tsx ← shop chrome │ └── products/page.tsx → /products

Use groups to apply different layouts to sibling routes or to colocate unrelated sections without nesting URLs.

app/ ├── (auth)/ │ ├── login/page.tsx → /login │ └── register/page.tsx → /register └── (dashboard)/ ├── layout.tsx ← authenticated chrome └── settings/page.tsx → /settings

Interview: Parentheses folders are organizational only — they never appear in the URL.

Parallel routes

Definition

Parallel routes use @slot folders to render multiple pages in the same layout simultaneously.

app/ ├── layout.tsx ├── @analytics/page.tsx ├── @team/page.tsx └── page.tsx
// app/dashboard/layout.tsx export default function DashboardLayout({ children, analytics, team, }: { children: React.ReactNode; analytics: React.ReactNode; team: React.ReactNode; }) { return ( <div className="grid"> <section>{children}</section> <aside>{analytics}</aside> <aside>{team}</aside> </div> ); }

Each slot can have its own loading.tsx and error.tsx — they stream independently.

Real-world example

Dashboard with independent panels: main content, analytics chart, and team list each suspend and error independently.

Interview: Parallel routes enable independent loading/error states per slot in one layout.

Intercepting routes

Definition

Intercepting routes render a route inside the current layout — typically as a modal — on soft navigation while still rendering a full page on hard navigation.

app/ ├── @modal/ │ └── (.)photo/[id]/page.tsx ← intercepts /photo/[id] as modal └── photo/[id]/page.tsx ← full page on direct visit

Convention prefixes:

(.) → same level (..) → one level up (..)(..) → two levels up (...) → from app root

Photo gallery pattern: click thumbnail → modal overlay; share URL → full photo page.

Interview: Intercepting routes only apply on client-side navigation. Direct URL visits render the full page route.

Data fetching migration

Definition

Pages RouterApp Router
getStaticProps + revalidateexport const revalidate = N or fetch(..., { next: { revalidate: N } })
getServerSidePropsexport const dynamic = 'force-dynamic' or noStore()
getStaticPathsgenerateStaticParams
// app/blog/[slug]/page.tsx — ISR + static params export async function generateStaticParams() { const slugs = await fetchAllSlugs(); return slugs.map((slug) => ({ slug })); } export const revalidate = 300; export default async function PostPage(props: { params: Promise<{ slug: string }>; }) { const { slug } = await props.params; const post = await fetch(`https://cms.example.com/posts/${slug}`, { next: { tags: [`post-${slug}`] }, }).then((r) => r.json()); return <article>{post.title}</article>; }

No separate data export — fetch lives in the async Server Component.

Interview: getServerSideProps has no direct equivalent export. Use async RSC with cache: 'no-store' or noStore().

Migration considerations

Definition

Both routers coexist in one project during migration. Rules:

Same path in both routers → conflict — pick one pages/_app.tsx equivalent → app/layout.tsx Client providers → thin 'use client' wrapper in layout API routes → pages/api/* OR app/**/route.ts

Pages Router is not removed — App Router is recommended for new work.

pages/ and app/ cannot share the same URL — migrate once with redirects

Interview: Migration is incremental. Move route-by-route; use redirects for URL changes.

Senior Interview Summary

App Router

Server-first routing with RSC defaults, nested layouts, and colocated data fetching.

Pages Router

Legacy client-centric model with separate data exports. Still supported.

Route groups

(folder) organizes layouts without URL segments.

Parallel & intercepting routes

@slot renders multiple pages per layout. (.) intercepts for modal-style soft navigation.

Best mental model

Pages → "Fetch in exports, render in page." App → "Fetch where you render; server by default." Layout → "App layouts persist; Pages _app remounts." Dynamic → "[slug] folders; generateStaticParams prebuilds."

Practical rule

New projects → App Router Existing Pages app → migrate incrementally Need parallel routes → App Router only Modal on soft nav → intercepting routes + parallel @modal slot Simple API endpoint → either route.ts or pages/api

Last updated on