Overview
The App Router (Next.js 13+) replaced the Pages Router with a new file-system routing convention built on React Server Components. Layouts persist across navigations (not remounted), loading UI is built in, and the entire React tree is split between server and client components by default. The mental model shift: instead of getServerSideProps, you just async/await inside a Server Component.
Architecture
APP ROUTER FILE CONVENTIONS
════════════════════════════════════════════════════════
app/
├── layout.tsx ← Root layout (wraps ALL routes)
├── page.tsx ← / route
├── loading.tsx ← Suspense fallback for this segment
├── error.tsx ← Error boundary for this segment (client)
├── not-found.tsx ← 404 handler
├── template.tsx ← Like layout but remounts on nav
│
├── dashboard/
│ ├── layout.tsx ← Wraps all /dashboard/* routes (persists)
│ ├── page.tsx ← /dashboard
│ ├── loading.tsx ← /dashboard loading state
│ │
│ ├── (analytics)/ ← Route group: groups routes without URL segment
│ │ └── page.tsx ← /dashboard (same URL, grouped for layout sharing)
│ │
│ ├── @modal/ ← Parallel route slot: renders alongside main content
│ │ └── page.tsx ← Shown in layout's {modal} slot
│ │
│ └── settings/
│ ├── page.tsx ← /dashboard/settings
│ └── [...slug]/ ← Catch-all: /dashboard/settings/a/b/c
│ └── page.tsx
LAYOUT PERSISTENCE vs TEMPLATE
════════════════════════════════════════════════════════
layout.tsx:
├── Nav (persists — state preserved between route changes)
├── Sidebar (persists)
└── {children} ← only this changes on navigation
template.tsx:
└── Same as layout but REMOUNTS on every navigation
→ use for animations that need to reset
→ use for per-route state initialization (scroll position reset)
REACT TREE (Server + Client Split)
════════════════════════════════════════════════════════
Server Component (default) Client Component ('use client')
├── Can be async (await fetch) ├── Can use hooks (useState, useEffect)
├── Runs on server, no JS to client ├── Runs on server (SSR) AND client
├── Can import server-only libs ├── Cannot import server-only modules
└── Cannot use browser APIs └── Can access DOM, window
Server → Client boundary:
'use client' at the top of a file marks it AND all its children as client.
You can pass Server Components as children/props INTO Client Components.
Technical Implementation
Nested Layouts with Shared State
// app/layout.tsx — root layout, applies to all routes
import { Inter } from 'next/font/google';
import { Toaster } from '@/components/ui/toaster';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
<Toaster /> {/* portal component — always mounted */}
</body>
</html>
);
}
// app/dashboard/layout.tsx — dashboard layout, persists during dashboard navigation
import { Sidebar } from '@/components/dashboard/Sidebar';
import { getUserFromSession } from '@/lib/auth';
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
// Server Component — can fetch data directly
const user = await getUserFromSession(); // no need for API call from client
return (
<div className="flex">
<Sidebar user={user} /> {/* persists, state preserved on route changes */}
<main className="flex-1">{children}</main>
</div>
);
}
Loading UI with Streaming (Suspense)
// app/dashboard/orders/loading.tsx — automatically wraps page.tsx in <Suspense>
export default function OrdersLoading() {
return (
<div className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="h-16 bg-muted animate-pulse rounded" />
))}
</div>
);
}
// app/dashboard/orders/page.tsx — async Server Component, streams in
export default async function OrdersPage() {
// This suspends while fetching — loading.tsx shown meanwhile
const orders = await fetchOrders(); // awaiting = Suspense boundary triggers
return (
<ul>
{orders.map(order => <OrderRow key={order.id} order={order} />)}
</ul>
);
}
// Fine-grained Suspense within a page for partial streaming
import { Suspense } from 'react';
export default async function DashboardPage() {
// Critical data — await at page level
const summary = await fetchSummary();
return (
<div>
<SummaryCard data={summary} />
{/* Non-critical widgets stream in independently */}
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart /> {/* async Server Component */}
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders /> {/* streams in when ready */}
</Suspense>
</div>
);
}
Error Boundary
// app/dashboard/error.tsx — MUST be a Client Component
'use client';
import { useEffect } from 'react';
interface ErrorProps {
error: Error & { digest?: string };
reset: () => void; // retries rendering the segment
}
export default function ErrorBoundary({ error, reset }: ErrorProps) {
useEffect(() => {
// Log to error tracking service (Sentry, etc.)
console.error('Dashboard error:', error);
}, [error]);
return (
<div className="flex flex-col items-center p-8 gap-4">
<h2 className="text-lg font-semibold">Something went wrong</h2>
<p className="text-muted-foreground text-sm">{error.message}</p>
<button onClick={reset} className="px-4 py-2 bg-primary text-white rounded">
Try again
</button>
</div>
);
}
Interview Preparation
Q: How does layout.tsx differ from template.tsx?
layout.tsx wraps its segment's routes and is NOT remounted when navigating between routes in its subtree — its React state, DOM elements, and effects persist. This is what enables the navigation sidebar to remain stable while only the main content changes. template.tsx provides the same nesting and wrapping, but IS remounted on every navigation — a new instance is created. Use template when you need state to reset on navigation (scroll to top, reset form, trigger enter animations) or when you need useEffect to fire on every navigation rather than once.
Q: What is the difference between a Server Component and a Client Component in the App Router?
Server Components run only on the server (or at build time for static routes). They can be async, can await database queries or API calls directly, can import server-only modules (DB clients, filesystem access), and produce no JavaScript bundle for the client. Client Components have 'use client' at the top. They render on the server for the initial HTML (SSR) but also ship their JavaScript to the client for interactivity. They can use hooks (useState, useEffect), access browser APIs (window, localStorage), and attach event handlers. Server Components cannot use hooks. The key constraint: once you cross into a 'use client' component, all children in that subtree are also client components — you cannot import a Server Component inside a Client Component (though you can pass one as a children prop).
Q: When should you use route groups (groupName) vs parallel routes @slot?
Route groups (folder) use parentheses so the folder name doesn't appear in the URL. Use them to: share a layout between a subset of routes, organize files without affecting URL structure, or apply different root layouts to sections (auth layout vs app layout). Parallel routes @slot render multiple independent pages in the same layout simultaneously — each slot has its own loading, error, and navigation state. Use them for: modals that have their own URL (open /photo/42 while main content is still /feed), dashboards with multiple independently-streaming widgets, or a split view where two panels navigate independently.