Master the new App Router in Next.js 14 with this comprehensive guide covering routing, layouts, and advanced patterns.
Next.js 14 introduced significant improvements to the App Router, making it more stable and feature-rich. This comprehensive guide will help you master the new routing system and build powerful applications.
The App Router is built on React Server Components and provides a more intuitive file-based routing system. Unlike the Pages Router, it offers better performance and more flexible layouts.
app/
├── page.tsx # Home page (/)
├── about/
│ └── page.tsx # About page (/about)
├── blog/
│ ├── page.tsx # Blog listing (/blog)
│ └── [slug]/
│ └── page.tsx # Dynamic blog post (/blog/[slug])
└── layout.tsx # Root layout
app/
├── layout.tsx # Root layout
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
├── global.css # Global styles
└── favicon.ico # Favicon
Every app must have a root layout:
// app/layout.tsx
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'My App',
description: 'A Next.js 14 application',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<header>
<nav>Navigation</nav>
</header>
<main>{children}</main>
<footer>Footer</footer>
</body>
</html>
)
}
Create layouts that apply to specific route segments:
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="blog-container">
<aside className="blog-sidebar">
<h2>Blog Categories</h2>
{/* Category navigation */}
</aside>
<article className="blog-content">
{children}
</article>
</div>
)
}
Server Components run on the server and are rendered to HTML:
// app/posts/page.tsx
import { getPosts } from '@/lib/posts'
export default async function PostsPage() {
const posts = await getPosts() // Runs on server
return (
<div>
<h1>All Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
Use "use client" directive for interactive components:
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
)
}
Fetch data directly in Server Components:
// app/users/page.tsx
async function getUsers() {
const res = await fetch('https://api.example.com/users', {
cache: 'no-store' // Always fetch fresh data
})
if (!res.ok) {
throw new Error('Failed to fetch users')
}
return res.json()
}
export default async function UsersPage() {
const users = await getUsers()
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
)
}
Control how data is cached:
// Revalidate every hour
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }
})
// Always fetch fresh data
const data = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
// Cache indefinitely
const data = await fetch('https://api.example.com/data', {
next: { revalidate: false }
})
Create loading states for route segments:
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="loading-container">
<div className="spinner" />
<p>Loading dashboard...</p>
</div>
)
}
Handle errors gracefully:
// app/dashboard/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="error-container">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)
}
Custom 404 pages:
// app/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div className="not-found">
<h2>Not Found</h2>
<p>Could not find requested resource</p>
<Link href="/">Return Home</Link>
</div>
)
}
Create dynamic routes with brackets:
// app/posts/[slug]/page.tsx
interface PostPageProps {
params: { slug: string }
}
export default async function PostPage({ params }: PostPageProps) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
Handle multiple segments:
// app/docs/[...slug]/page.tsx
interface DocsPageProps {
params: { slug: string[] }
}
export default function DocsPage({ params }: DocsPageProps) {
const slug = params.slug.join('/')
return (
<div>
<h1>Documentation: {slug}</h1>
{/* Render documentation content */}
</div>
)
}
Render multiple pages simultaneously:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<div className="dashboard">
<div className="main-content">{children}</div>
<div className="sidebar">
{analytics}
{team}
</div>
</div>
)
}
Show modals for certain routes:
// app/@modal/(..)photo/[id]/page.tsx
export default function PhotoModal({
params,
}: {
params: { id: string }
}) {
return (
<div className="modal">
<h1>Photo {params.id}</h1>
{/* Modal content */}
</div>
)
}
Define metadata at build time:
// app/about/page.tsx
export const metadata = {
title: 'About Us',
description: 'Learn more about our company',
openGraph: {
title: 'About Us',
description: 'Learn more about our company',
images: ['/og-image.jpg'],
},
}
export default function AboutPage() {
return <div>About content</div>
}
Generate metadata based on data:
// app/posts/[slug]/page.tsx
interface PostPageProps {
params: { slug: string }
}
export async function generateMetadata({ params }: PostPageProps) {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.image],
},
}
}
Progressive page loading:
// app/dashboard/page.tsx
import { Suspense } from 'react'
async function SlowComponent() {
await new Promise(resolve => setTimeout(resolve, 2000))
return <div>Slow content loaded!</div>
}
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading slow content...</div>}>
<SlowComponent />
</Suspense>
</div>
)
}
Automatic code splitting with dynamic imports:
'use client'
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <p>Loading...</p>,
ssr: false
})
export default function Page() {
return (
<div>
<h1>My Page</h1>
<HeavyComponent />
</div>
)
}
// Pages Router
export async function getServerSideProps() {
const data = await fetchData()
return { props: { data } }
}
// App Router
export default async function Page() {
const data = await fetchData()
return <div>{/* render data */}</div>
}
The Next.js 14 App Router represents a significant evolution in React-based web development. By mastering these concepts and patterns, you can build more performant, maintainable, and user-friendly applications.
Key takeaways:
Start experimenting with these patterns in your projects, and you'll quickly see the benefits of the new App Router architecture.