Next.js App Router
App Router is the new routing system introduced in Next.js 13, built on React Server Components, providing more powerful features and a better development experience. This chapter will detail the core concepts and usage of App Router.
App Router Overview
Main Features
- Server Components - Rendered on the server by default for better performance
- Nested Layouts - Support for complex layout structures
- Streaming - Progressive page content loading
- Parallel Routes - Render multiple page segments simultaneously
- Intercepting Routes - Intercept and rewrite routes
- Better Data Fetching - Simplified async components
Directory Structure
app/
├── layout.tsx # Root layout
├── page.tsx # Homepage
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
├── global-error.tsx # Global error handling
├── blog/
│ ├── layout.tsx # Blog layout
│ ├── page.tsx # Blog homepage
│ ├── loading.tsx # Blog loading UI
│ └── [slug]/
│ └── page.tsx # Blog post page
└── dashboard/
├── layout.tsx # Dashboard layout
├── page.tsx # Dashboard homepage
├── analytics/
│ └── page.tsx # Analytics page
└── settings/
└── page.tsx # Settings pageFile Conventions
Special Files
| Filename | Purpose | Required |
|---|---|---|
layout.tsx | Layout component | ✅ |
page.tsx | Page component | ✅ |
loading.tsx | Loading UI | ❌ |
error.tsx | Error UI | ❌ |
not-found.tsx | 404 UI | ❌ |
global-error.tsx | Global error UI | ❌ |
route.tsx | API route | ❌ |
template.tsx | Template component | ❌ |
Dynamic Routes
app/
├── [id]/ # Dynamic route segment
├── [...slug]/ # Catch-all route
├── [[...slug]]/ # Optional catch-all route
└── (group)/ # Route group (doesn't affect URL)Layout System
Root Layout
typescript
// app/layout.tsx
import './globals.css'
export const metadata = {
title: 'My App',
description: 'Generated by Next.js',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<header>
<nav>Navigation</nav>
</header>
<main>{children}</main>
<footer>Footer</footer>
</body>
</html>
)
}Nested Layouts
typescript
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="dashboard">
<aside className="sidebar">
<nav>
<a href="/dashboard">Overview</a>
<a href="/dashboard/analytics">Analytics</a>
<a href="/dashboard/settings">Settings</a>
</nav>
</aside>
<div className="content">
{children}
</div>
</div>
)
}Layout Composition
typescript
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="blog-layout">
<div className="blog-header">
<h1>My Blog</h1>
</div>
<div className="blog-content">
{children}
</div>
<div className="blog-sidebar">
<h3>Recent Posts</h3>
{/* Sidebar content */}
</div>
</div>
)
}Server Components
Default Server Components
typescript
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
<h1>Posts List</h1>
{posts.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}Parallel Data Fetching
typescript
// app/dashboard/page.tsx
async function getUser() {
const res = await fetch('https://api.example.com/user')
return res.json()
}
async function getStats() {
const res = await fetch('https://api.example.com/stats')
return res.json()
}
async function getNotifications() {
const res = await fetch('https://api.example.com/notifications')
return res.json()
}
export default async function Dashboard() {
// Fetch data in parallel
const [user, stats, notifications] = await Promise.all([
getUser(),
getStats(),
getNotifications()
])
return (
<div>
<h1>Welcome, {user.name}</h1>
<div className="stats">
<div>Visits: {stats.visits}</div>
<div>Users: {stats.users}</div>
</div>
<div className="notifications">
{notifications.map((notif: any) => (
<div key={notif.id}>{notif.message}</div>
))}
</div>
</div>
)
}Client Components
Using 'use client' Directive
typescript
// app/components/Counter.tsx
'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>
)
}Mixing Server and Client Components
typescript
// app/blog/[slug]/page.tsx (Server Component)
import CommentForm from './CommentForm'
import LikeButton from './LikeButton'
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`)
return res.json()
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* Client Components */}
<LikeButton postId={post.id} />
<CommentForm postId={post.id} />
</article>
)
}typescript
// app/blog/[slug]/LikeButton.tsx (Client Component)
'use client'
import { useState } from 'react'
export default function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false)
const [likes, setLikes] = useState(0)
const handleLike = async () => {
const response = await fetch(`/api/posts/${postId}/like`, {
method: 'POST'
})
const data = await response.json()
setLiked(!liked)
setLikes(data.likes)
}
return (
<button
onClick={handleLike}
className={`like-btn ${liked ? 'liked' : ''}`}
>
❤️ {likes}
</button>
)
}Streaming and Suspense
Basic Suspense Usage
typescript
// app/dashboard/page.tsx
import { Suspense } from 'react'
import UserProfile from './UserProfile'
import RecentActivity from './RecentActivity'
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading user info...</div>}>
<UserProfile />
</Suspense>
<Suspense fallback={<div>Loading activity...</div>}>
<RecentActivity />
</Suspense>
</div>
)
}Nested Suspense
typescript
// app/blog/page.tsx
import { Suspense } from 'react'
async function FeaturedPosts() {
const posts = await fetchFeaturedPosts()
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
<Suspense fallback={<div>Loading comments...</div>}>
<Comments postId={post.id} />
</Suspense>
</article>
))}
</div>
)
}
async function Comments({ postId }: { postId: string }) {
const comments = await fetchComments(postId)
return (
<div>
{comments.map(comment => (
<div key={comment.id}>{comment.text}</div>
))}
</div>
)
}
export default function BlogPage() {
return (
<div>
<h1>Blog</h1>
<Suspense fallback={<div>Loading featured posts...</div>}>
<FeaturedPosts />
</Suspense>
</div>
)
}Loading and Error Handling
Loading UI
typescript
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="loading">
<div className="spinner"></div>
<p>Loading blog content...</p>
</div>
)
}Error Handling
typescript
// app/blog/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="error">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>
Try again
</button>
</div>
)
}Global Error Handling
typescript
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<div className="global-error">
<h1>Application Error</h1>
<p>An unexpected error occurred</p>
<button onClick={() => reset()}>Try again</button>
</div>
</body>
</html>
)
}Route Groups
Organizing Route Structure
app/
├── (marketing)/ # Marketing pages group
│ ├── about/
│ │ └── page.tsx
│ ├── contact/
│ │ └── page.tsx
│ └── layout.tsx # Marketing layout
├── (shop)/ # Shop pages group
│ ├── products/
│ │ └── page.tsx
│ ├── cart/
│ │ └── page.tsx
│ └── layout.tsx # Shop layout
└── layout.tsx # Root layouttypescript
// app/(marketing)/layout.tsx
export default function MarketingLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="marketing-layout">
<nav className="marketing-nav">
<a href="/about">About Us</a>
<a href="/contact">Contact Us</a>
</nav>
{children}
</div>
)
}Parallel Routes
Using @folder Syntax
app/
├── dashboard/
│ ├── @analytics/ # Parallel route segment
│ │ └── page.tsx
│ ├── @team/ # Parallel route segment
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsxtypescript
// 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">{children}</div>
<div className="analytics">{analytics}</div>
<div className="team">{team}</div>
</div>
)
}Intercepting Routes
Using (.) Syntax
app/
├── feed/
│ ├── (..)photo/ # Intercept /photo
│ │ └── [id]/
│ │ └── page.tsx
│ └── page.tsx
└── photo/
└── [id]/
└── page.tsxtypescript
// app/feed/(..)photo/[id]/page.tsx
import Modal from '@/components/Modal'
export default function PhotoModal({ params }: { params: { id: string } }) {
return (
<Modal>
<img src={`/photos/${params.id}.jpg`} alt="Photo" />
</Modal>
)
}Metadata API
Static Metadata
typescript
// app/blog/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My Blog',
description: 'Sharing tech and life',
keywords: ['blog', 'tech', 'Next.js'],
openGraph: {
title: 'My Blog',
description: 'Sharing tech and life',
images: ['/og-image.jpg'],
},
}
export default function BlogPage() {
return <div>Blog content</div>
}Dynamic Metadata
typescript
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
interface Props {
params: { slug: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await fetchPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
export default async function BlogPost({ params }: Props) {
const post = await fetchPost(params.slug)
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}Middleware Integration
Middleware Configuration
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Check authentication
const token = request.cookies.get('token')
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
// Add custom header
const response = NextResponse.next()
response.headers.set('X-Custom-Header', 'custom-value')
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*']
}Performance Optimization
Prefetching Strategy
typescript
// app/components/PostLink.tsx
'use client'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
export default function PostLink({ post }: { post: Post }) {
const router = useRouter()
return (
<Link
href={`/blog/${post.slug}`}
onMouseEnter={() => {
// Prefetch page
router.prefetch(`/blog/${post.slug}`)
}}
>
{post.title}
</Link>
)
}Cache Configuration
typescript
// app/api/posts/route.ts
export const revalidate = 3600 // Revalidate every hour
export async function GET() {
const posts = await fetchPosts()
return Response.json(posts)
}Migration Guide
Migrating from Pages Router
- Create app directory
- Move page files
- Update layout structure
- Convert data fetching methods
- Update API routes
typescript
// Old: pages/blog/[slug].tsx
export async function getStaticProps({ params }) {
const post = await fetchPost(params.slug)
return { props: { post } }
}
export default function BlogPost({ post }) {
return <div>{post.title}</div>
}
// New: app/blog/[slug]/page.tsx
export default async function BlogPost({ params }) {
const post = await fetchPost(params.slug)
return <div>{post.title}</div>
}Best Practices
1. Use Server and Client Components Wisely
typescript
// Server Component for data fetching
async function PostList() {
const posts = await fetchPosts()
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
)
}
// Client Component for interactivity
'use client'
function PostCard({ post }) {
const [liked, setLiked] = useState(false)
return (
<div>
<h3>{post.title}</h3>
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
</div>
)
}2. Optimize Loading Experience
typescript
// Use Suspense for better loading experience
export default function Page() {
return (
<div>
<h1>Page Title</h1>
<Suspense fallback={<PostsSkeleton />}>
<PostList />
</Suspense>
</div>
)
}3. Error Boundary Strategy
typescript
// Provide error handling for different levels
// app/error.tsx - App level errors
// app/blog/error.tsx - Blog module errors
// app/blog/[slug]/error.tsx - Post page errorsSummary
App Router brings many improvements:
- Better performance - Server Components and streaming
- More flexible layouts - Nested layouts and parallel routes
- Simpler data fetching - Async components and parallel requests
- Better user experience - Loading states and error handling
- Stronger type safety - Full TypeScript support
App Router is the future direction of Next.js. It's recommended for new projects, and existing projects can migrate gradually.