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.
| Aspect | Pages Router | App Router |
|---|---|---|
| Default component type | Client | Server Component |
| Data fetching | getStaticProps / getServerSideProps exports | Async Server Components + fetch |
| Layouts | _app.tsx remounts; getLayout pattern | Nested layout.tsx persists on nav |
| Loading / error UI | Manual Suspense / error boundaries | loading.tsx, error.tsx conventions |
| Recommendation | Maintained, legacy | Default 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.tsxInterview: 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 unchangedInterview: 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 slugsReal-world example
Docs site with thousands of pages: prebuild top 100, generate the rest on first visit.
Interview:
paramsis 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 → /productsUse 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 → /settingsInterview: 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 visitConvention prefixes:
(.) → same level
(..) → one level up
(..)(..) → two levels up
(...) → from app rootPhoto 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 Router | App Router |
|---|---|
getStaticProps + revalidate | export const revalidate = N or fetch(..., { next: { revalidate: N } }) |
getServerSideProps | export const dynamic = 'force-dynamic' or noStore() |
getStaticPaths | generateStaticParams |
// 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:
getServerSidePropshas no direct equivalent export. Use async RSC withcache: 'no-store'ornoStore().
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.tsPages Router is not removed — App Router is recommended for new work.
pages/ and app/ cannot share the same URL — migrate once with redirectsInterview: 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
@slotrenders 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