Next.js App Router Complete Guide: From Basics to Advanced Patterns
Master the Next.js App Router with this comprehensive guide covering routing, layouts, server components, data fetching, and advanced patterns for building modern web applications.
Next.js App Router Complete Guide: From Basics to Advanced Patterns#
The Next.js App Router represents a fundamental shift in how we build React applications. This comprehensive guide will take you from the basics to advanced patterns, helping you master modern Next.js development.
Table of Contents#
- Understanding the App Router
- File-Based Routing
- Layouts and Templates
- Server and Client Components
- Data Fetching Strategies
- Route Handlers (API Routes)
- Advanced Patterns
Understanding the App Router#
The App Router is built on React Server Components and introduces new conventions for building applications with improved performance and developer experience.
Key Benefits#
- Server-First Architecture: Components render on the server by default
- Streaming and Suspense: Progressive rendering for better UX
- Nested Layouts: Share UI across routes efficiently
- Colocation: Keep related files together
- Improved Data Fetching: Fetch data where you need it
File-Based Routing#
Basic Route Structure#
app/
├── page.js # / route
├── about/
│ └── page.js # /about route
├── blog/
│ ├── page.js # /blog route
│ └── [slug]/
│ └── page.js # /blog/[slug] dynamic route
└── dashboard/
├── layout.js # Dashboard layout
├── page.js # /dashboard route
└── settings/
└── page.js # /dashboard/settings route
Dynamic Routes#
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
return <h1>Post: {params.slug}</h1>
}
// Generate static params at build time
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
Catch-All Routes#
// app/docs/[...slug]/page.js
export default function Docs({ params }) {
// params.slug will be an array: ['getting-started', 'installation']
return <div>Docs: {params.slug.join('/')}</div>
}
Layouts and Templates#
Root Layout (Required)#
// app/layout.js
export const metadata = {
title: 'My App',
description: 'Welcome to my app',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>
<nav>{/* Navigation */}</nav>
</header>
<main>{children}</main>
<footer>{/* Footer */}</footer>
</body>
</html>
)
}
Nested Layouts#
// app/dashboard/layout.js
export default function DashboardLayout({ children }) {
return (
<div className="dashboard">
<aside>
<DashboardNav />
</aside>
<section>{children}</section>
</div>
)
}
Templates (Re-render on Navigation)#
// app/template.js
export default function Template({ children }) {
return <div className="animate-fade-in">{children}</div>
}
Server and Client Components#
Server Components (Default)#
// app/posts/page.js - Server Component
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store', // Dynamic data
})
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
Client Components#
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
)
}
Composition Pattern#
// app/page.js - Server Component
import ClientComponent from './ClientComponent'
async function getData() {
const data = await fetch('...')
return data.json()
}
export default async function Page() {
const data = await getData()
return (
<div>
<h1>Server-rendered content</h1>
<ClientComponent initialData={data} />
</div>
)
}
Data Fetching Strategies#
Static Data (Default)#
// Cached by default
async function getStaticData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
Dynamic Data#
// Opt out of caching
async function getDynamicData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
return res.json()
}
Revalidated Data#
// Revalidate every 60 seconds
async function getRevalidatedData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
return res.json()
}
Parallel Data Fetching#
export default async function Page() {
// Fetch in parallel
const [posts, users] = await Promise.all([
getPosts(),
getUsers()
])
return (
<div>
<Posts data={posts} />
<Users data={users} />
</div>
)
}
Sequential Data Fetching#
export default async function Page() {
const user = await getUser()
const posts = await getUserPosts(user.id) // Waits for user
return <UserPosts user={user} posts={posts} />
}
Route Handlers#
Basic API Route#
// app/api/posts/route.js
export async function GET(request) {
const posts = await getPosts()
return Response.json(posts)
}
export async function POST(request) {
const body = await request.json()
const post = await createPost(body)
return Response.json(post, { status: 201 })
}
Dynamic Route Handlers#
// app/api/posts/[id]/route.js
export async function GET(request, { params }) {
const post = await getPost(params.id)
if (!post) {
return Response.json({ error: 'Not found' }, { status: 404 })
}
return Response.json(post)
}
export async function PATCH(request, { params }) {
const body = await request.json()
const post = await updatePost(params.id, body)
return Response.json(post)
}
export async function DELETE(request, { params }) {
await deletePost(params.id)
return new Response(null, { status: 204 })
}
Middleware Integration#
// app/api/protected/route.js
import { auth } from '@/lib/auth'
export async function GET(request) {
const session = await auth(request)
if (!session) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const data = await getProtectedData(session.userId)
return Response.json(data)
}
Advanced Patterns#
Loading States#
// app/dashboard/loading.js
export default function Loading() {
return <div className="spinner">Loading...</div>
}
Error Handling#
// app/dashboard/error.js
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
Not Found Pages#
// app/blog/[slug]/not-found.js
export default function NotFound() {
return (
<div>
<h2>Post Not Found</h2>
<p>Could not find the requested post.</p>
</div>
)
}
// Trigger not-found
import { notFound } from 'next/navigation'
export default async function Page({ params }) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return <Post data={post} />
}
Streaming with Suspense#
import { Suspense } from 'react'
async function SlowComponent() {
await new Promise(resolve => setTimeout(resolve, 3000))
return <div>Slow content loaded!</div>
}
export default function Page() {
return (
<div>
<h1>Fast content</h1>
<Suspense fallback={<div>Loading slow content...</div>}>
<SlowComponent />
</Suspense>
</div>
)
}
Route Groups#
app/
├── (marketing)/
│ ├── layout.js # Marketing layout
│ ├── page.js # / route
│ └── about/
│ └── page.js # /about route
└── (shop)/
├── layout.js # Shop layout
├── products/
│ └── page.js # /products route
└── cart/
└── page.js # /cart route
Parallel Routes#
app/
└── dashboard/
├── @analytics/
│ └── page.js
├── @team/
│ └── page.js
└── layout.js
// app/dashboard/layout.js
export default function Layout({ children, analytics, team }) {
return (
<div>
{children}
<div className="grid grid-cols-2">
{analytics}
{team}
</div>
</div>
)
}
Intercepting Routes#
app/
└── photos/
├── [id]/
│ └── page.js # /photos/123
└── (..)photos/
└── [id]/
└── page.js # Intercepts /photos/123 when navigating from same level
Best Practices#
1. Server Components by Default#
Use server components unless you need interactivity, browser APIs, or state.
2. Fetch Data Where You Need It#
Don't prop drill - fetch data in the component that needs it.
3. Use Streaming for Better UX#
Wrap slow components in Suspense to stream content progressively.
4. Optimize Images#
import Image from 'next/image'
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
/>
5. Implement Proper Error Boundaries#
Create error.js files at appropriate levels in your route hierarchy.
6. Use Metadata API#
export const metadata = {
title: 'My Page',
description: 'Page description',
openGraph: {
title: 'My Page',
description: 'Page description',
images: ['/og-image.jpg'],
},
}
Performance Optimization#
Code Splitting#
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <p>Loading...</p>,
ssr: false, // Disable SSR if needed
})
Font Optimization#
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
)
}
Route Prefetching#
Next.js automatically prefetches routes in the viewport using <Link>.
Conclusion#
The Next.js App Router provides a powerful foundation for building modern web applications. By understanding these patterns and best practices, you can create fast, scalable, and maintainable applications.
Key Takeaways#
- Server Components are the default and provide better performance
- Use Client Components only when needed for interactivity
- Leverage streaming and Suspense for better UX
- Fetch data where you need it, not at the top level
- Use proper error handling and loading states
- Optimize images, fonts, and code splitting
Next Steps#
- Explore the Next.js documentation
- Build a project using the App Router
- Experiment with advanced patterns like parallel routes
- Learn about deployment optimization
Related Guides: