Next.js App Router, introduced in Next.js 13, represents a fundamental shift in how we build React applications. It leverages React Server Components and provides an intuitive file-system based routing approach.
Why App Router?
flowchart TD
subgraph Pages["Pages Router (Legacy)"]
A[pages/index.js] --> B[pages/about.js]
B --> C[pages/blog/[slug].js]
end
subgraph App["App Router (Modern)"]
D[app/page.tsx] --> E[app/about/page.tsx]
E --> F[app/blog/[slug]/page.tsx]
G[app/layout.tsx] --> D
G --> E
G --> F
end
style Pages fill:#f59e0b,color:#fff
style App fill:#10b981,color:#fff
| Feature | Pages Router | App Router |
|---|---|---|
| Server Components | No | Yes (default) |
| Nested Layouts | Limited | Full support |
| Streaming | No | Yes |
| Data Fetching | getServerSideProps | async components |
Project Structure
app/
βββ layout.tsx # Root layout (required)
βββ page.tsx # Home page (/)
βββ loading.tsx # Loading UI
βββ error.tsx # Error UI
βββ not-found.tsx # 404 page
βββ about/
β βββ page.tsx # About page (/about)
βββ blog/
β βββ page.tsx # Blog index (/blog)
β βββ [slug]/
β βββ page.tsx # Blog post (/blog/my-post)
βββ (marketing)/ # Route group
βββ pricing/
β βββ page.tsx # /pricing
βββ features/
βββ page.tsx # /features
File Conventions
page.tsx - Route UI
Every route needs a page.tsx file to be publicly accessible:
// app/page.tsx - Home page
export default function HomePage() {
return (
<main>
<h1>Welcome to My Site</h1>
<p>This is the home page.</p>
</main>
);
}
// app/about/page.tsx - About page
export default function AboutPage() {
return (
<main>
<h1>About Us</h1>
<p>Learn more about our company.</p>
</main>
);
}
layout.tsx - Shared UI
Layouts wrap pages and persist across navigations:
// app/layout.tsx - Root layout
import { ReactNode } from 'react';
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<header>
<nav>Site Navigation</nav>
</header>
<main>{children}</main>
<footer>Β© 2025 My Site</footer>
</body>
</html>
);
}
// app/blog/layout.tsx - Blog-specific layout
import { ReactNode } from 'react';
export default function BlogLayout({ children }: { children: ReactNode }) {
return (
<div className="blog-container">
<aside>Blog Sidebar</aside>
<article>{children}</article>
</div>
);
}
loading.tsx - Loading State
Automatic loading UI while content loads:
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="loading">
<div className="spinner" />
<p>Loading posts...</p>
</div>
);
}
error.tsx - Error Boundary
Handle errors gracefully:
// app/blog/error.tsx
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="error">
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
not-found.tsx - 404 Page
Custom 404 pages:
// app/not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<div>
<h2>Page Not Found</h2>
<p>Could not find the requested resource.</p>
<Link href="/">Return Home</Link>
</div>
);
}
Dynamic Routes
Basic Dynamic Routes
// app/blog/[slug]/page.tsx
type Props = {
params: Promise<{ slug: string }>;
};
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
return (
<article>
<h1>Blog Post: {slug}</h1>
</article>
);
}
Catch-All Routes
// app/docs/[...slug]/page.tsx - Matches /docs/a, /docs/a/b, /docs/a/b/c
type Props = {
params: Promise<{ slug: string[] }>;
};
export default async function DocsPage({ params }: Props) {
const { slug } = await params;
// slug is ['a', 'b', 'c'] for /docs/a/b/c
return (
<div>
<h1>Documentation</h1>
<p>Path: {slug.join('/')}</p>
</div>
);
}
Optional Catch-All Routes
// app/shop/[[...slug]]/page.tsx - Also matches /shop
type Props = {
params: Promise<{ slug?: string[] }>;
};
export default async function ShopPage({ params }: Props) {
const { slug } = await params;
if (!slug) {
return <h1>All Products</h1>;
}
return <h1>Category: {slug.join(' > ')}</h1>;
}
Navigation
Link Component
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
{/* Basic navigation */}
<Link href="/">Home</Link>
<Link href="/about">About</Link>
{/* Dynamic routes */}
<Link href="/blog/my-first-post">First Post</Link>
{/* With query params */}
<Link href="/search?q=react">Search React</Link>
{/* Replace history instead of push */}
<Link href="/dashboard" replace>
Dashboard
</Link>
{/* Prefetch disabled */}
<Link href="/heavy-page" prefetch={false}>
Heavy Page
</Link>
</nav>
);
}
Active Link Styling
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
export default function NavLink({
href,
children,
}: {
href: string;
children: React.ReactNode;
}) {
const pathname = usePathname();
const isActive = pathname === href;
return (
<Link
href={href}
className={isActive ? 'nav-link active' : 'nav-link'}
>
{children}
</Link>
);
}
Programmatic Navigation
'use client';
import { useRouter } from 'next/navigation';
export default function LoginForm() {
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// ... login logic
// Navigate after login
router.push('/dashboard');
// Or replace (no back navigation)
router.replace('/dashboard');
// Go back
router.back();
// Refresh current route
router.refresh();
};
return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}
Route Groups
Route groups organize routes without affecting the URL:
app/
βββ (marketing)/
β βββ layout.tsx # Marketing layout
β βββ about/page.tsx # /about
β βββ pricing/page.tsx # /pricing
βββ (shop)/
β βββ layout.tsx # Shop layout
β βββ products/page.tsx # /products
β βββ cart/page.tsx # /cart
βββ (auth)/
βββ layout.tsx # Auth layout
βββ login/page.tsx # /login
βββ signup/page.tsx # /signup
// app/(marketing)/layout.tsx
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="marketing-layout">
<header>Marketing Header</header>
{children}
</div>
);
}
Parallel Routes
Render multiple pages simultaneously in the same layout:
app/
βββ layout.tsx
βββ page.tsx
βββ @team/
β βββ page.tsx
βββ @analytics/
βββ page.tsx
// app/layout.tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<div>
{children}
<div className="dashboard-panels">
{team}
{analytics}
</div>
</div>
);
}
Metadata
Static Metadata
// app/about/page.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'About Us',
description: 'Learn more about our company',
openGraph: {
title: 'About Us',
description: 'Learn more about our company',
},
};
export default function AboutPage() {
return <h1>About Us</h1>;
}
Dynamic Metadata
// app/blog/[slug]/page.tsx
import { Metadata } from 'next';
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
};
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}
Static Generation
generateStaticParams
Pre-render dynamic routes at build time:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}
Summary
| File | Purpose |
|---|---|
page.tsx |
Route UI component |
layout.tsx |
Shared layout wrapper |
loading.tsx |
Loading state UI |
error.tsx |
Error boundary |
not-found.tsx |
404 page |
[slug] |
Dynamic route segment |
[...slug] |
Catch-all route |
(group) |
Route group (no URL impact) |
@slot |
Parallel route slot |
Key takeaways:
- App Router uses file-system based routing with special file conventions
- Layouts persist across navigations and can be nested
- Dynamic routes use bracket notation:
[slug],[...slug],[[...slug]] - Use
Linkfor navigation anduseRouterfor programmatic navigation - Route groups
(name)organize code without affecting URLs - Metadata can be static or dynamically generated
generateStaticParamsenables static generation for dynamic routes
The App Router provides a more intuitive and powerful routing system that integrates seamlessly with React Server Components.
References
- Next.js App Router Documentation
- Next.js Routing Fundamentals
- SchwarzmΓΌller, Maximilian. React Key Concepts - Second Edition. Packt, 2025.