Next.js App Router: The Complete Guide
The App Router is a paradigm shift in Next.js that brings powerful new features like layouts, nested routing, and enhanced server-side rendering capabilities. Let's dive deep into how to leverage these features effectively.
Understanding the App Directory Structure
The App Router uses a file-system based routing where folders define routes and special files define UI:
app/
├── layout.tsx # Root layout
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
└── dashboard/
├── layout.tsx # Dashboard layout
├── page.tsx # Dashboard page
├── settings/
│ └── page.tsx # Settings page
└── analytics/
└── page.tsx # Analytics page
Creating Layouts
Layouts are shared UI that wrap multiple pages:
// app/layout.tsx
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'My App',
description: 'Generated by Next.js',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<nav className="bg-blue-600 text-white p-4">
<h1>My Application</h1>
</nav>
<main className="container mx-auto p-4">
{children}
</main>
</body>
</html>
)
}
Nested Layouts
Create section-specific layouts that only apply to certain routes:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex">
<aside className="w-64 bg-gray-100 min-h-screen p-4">
<nav>
<ul className="space-y-2">
<li><a href="/dashboard">Overview</a></li>
<li><a href="/dashboard/analytics">Analytics</a></li>
<li><a href="/dashboard/settings">Settings</a></li>
</ul>
</nav>
</aside>
<div className="flex-1 p-8">
{children}
</div>
</div>
)
}
Dynamic Routes
Create dynamic routes using square brackets:
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return (
<article>
<h1>Blog Post: {params.slug}</h1>
{/* Fetch and display blog content */}
</article>
)
}
// Generate static params for static generation
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json())
return posts.map((post: any) => ({
slug: post.slug,
}))
}
Server and Client Components
By default, components in the App Router are Server Components:
// app/posts/page.tsx (Server Component)
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store' // This will fetch fresh data on every request
})
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
<h1>Latest Posts</h1>
{posts.map((post: any) => (
<article key={post.id} className="mb-6">
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
For interactive components, use the 'use client'
directive:
// components/search-form.tsx (Client Component)
'use client'
import { useState } from 'react'
export default function SearchForm() {
const [query, setQuery] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Handle search logic
console.log('Searching for:', query)
}
return (
<form onSubmit={handleSubmit} className="mb-6">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search posts..."
className="border p-2 rounded mr-2"
/>
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
Search
</button>
</form>
)
}
Loading and Error States
Create better user experiences with loading and error boundaries:
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="flex justify-center items-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900"></div>
</div>
)
}
// app/dashboard/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="text-center py-8">
<h2 className="text-2xl font-bold text-red-600 mb-4">Something went wrong!</h2>
<p className="text-gray-600 mb-4">{error.message}</p>
<button
onClick={reset}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Try again
</button>
</div>
)
}
Data Fetching Patterns
The App Router provides several ways to fetch data:
// Static data fetching
async function getStaticData() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache' // This is the default
})
return res.json()
}
// Dynamic data fetching
async function getDynamicData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
return res.json()
}
// Revalidated data fetching
async function getRevalidatedData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // Revalidate every hour
})
return res.json()
}
Route Groups
Organize routes without affecting URL structure using parentheses:
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx
│ └── contact/
│ └── page.tsx
└── (shop)/
├── products/
│ └── page.tsx
└── cart/
└── page.tsx
Migration from Pages Router
To migrate from the Pages Router:
- Move pages from
pages/
toapp/
directory - Convert to layouts instead of
_app.tsx
- Update data fetching from
getServerSideProps
/getStaticProps
to async components - Add
'use client'
directive to components that need client-side features
Best Practices
- Use Server Components by default - only add
'use client'
when needed - Colocate related files - keep components, styles, and tests near the routes that use them
- Leverage layouts for shared UI and avoid prop drilling
- Use loading.tsx for better perceived performance
- Implement proper error boundaries with error.tsx files
- Optimize data fetching using appropriate caching strategies
The App Router represents the future of Next.js development, providing better performance, developer experience, and more intuitive patterns for building modern web applications.