App Router Guide
Modern routing with React Server Components
What is App Router?
The App Router is a new paradigm in Next.js 13+ that uses React Server Components and provides advanced routing features.
App vs Pages Router
| Feature | App Router (New) | Pages Router (Legacy) |
|---|---|---|
| Directory | app/ |
pages/ |
| Server Components | ✅ Default | ❌ Not available |
| Nested Layouts | ✅ Built-in | ❌ Manual |
| Streaming | ✅ Yes | ❌ No |
| Data Fetching | async/await in components | getServerSideProps, etc. |
File-based Routing
Routes are defined by the folder structure in the app/ directory.
Basic Routes
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ └── page.tsx → /blog
└── contact/
└── page.tsx → /contact
Dynamic Routes
app/
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/:slug
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>Post: {params.slug}</h1>
}
Catch-all Routes
app/
└── docs/
└── [...slug]/
└── page.tsx → /docs/a, /docs/a/b, /docs/a/b/c
// app/docs/[...slug]/page.tsx
export default function Docs({ params }: { params: { slug: string[] } }) {
return <h1>Docs: {params.slug.join('/')}</h1>
}
Route Groups
Organize routes without affecting URL structure using (folder):
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx → /about
│ └── blog/
│ └── page.tsx → /blog
└── (shop)/
├── products/
│ └── page.tsx → /products
└── cart/
└── page.tsx → /cart
Private Folders
Prefix with underscore to exclude from routing:
app/
├── _components/ → Not a route
│ └── Button.tsx
└── dashboard/
└── page.tsx → /dashboard
Layouts
Layouts are UI that is shared between routes. They preserve state and don't re-render.
Root Layout (Required)
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<header>Global Header</header>
{children}
<footer>Global Footer</footer>
</body>
</html>
)
}
Nested Layouts
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div>
<aside>Dashboard Sidebar</aside>
<main>{children}</main>
</div>
)
}
// app/dashboard/page.tsx will be wrapped by both layouts
Layout Hierarchy
app/
├── layout.tsx (Root Layout)
├── page.tsx (Home Page)
└── dashboard/
├── layout.tsx (Dashboard Layout)
├── page.tsx (Dashboard Page)
└── settings/
└── page.tsx (Settings Page)
// Rendering hierarchy for /dashboard/settings:
// RootLayout → DashboardLayout → SettingsPage
Pages
A page is UI that is unique to a route. Define by exporting a component from a page.tsx file.
Basic Page
// app/about/page.tsx
export default function AboutPage() {
return <h1>About Us</h1>
}
Page with Params
// app/blog/[slug]/page.tsx
export default function BlogPost({
params,
searchParams,
}: {
params: { slug: string }
searchParams: { [key: string]: string | string[] | undefined }
}) {
return (
<div>
<h1>Post: {params.slug}</h1>
<p>Sort: {searchParams.sort}</p>
</div>
)
}
// URL: /blog/hello-world?sort=asc
// params.slug = "hello-world"
// searchParams.sort = "asc"
Metadata (SEO)
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
export async function generateMetadata({
params,
}: {
params: { slug: string }
}): Promise<Metadata> {
return {
title: `Blog Post: ${params.slug}`,
description: 'Blog post description',
}
}
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>
}
Loading UI & Streaming
Create instant loading states with loading.tsx and stream content as it becomes ready.
Loading File
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div>
<h2>Loading...</h2>
<div className="spinner"></div>
</div>
)
}
Streaming with Suspense
import { Suspense } from 'react'
async function SlowComponent() {
// Slow data fetch
await new Promise(resolve => setTimeout(resolve, 3000))
return <div>Loaded!</div>
}
export default function Page() {
return (
<div>
<h1>Page Content</h1>
<Suspense fallback={<div>Loading slow component...</div>}>
<SlowComponent />
</Suspense>
</div>
)
}
Error Handling
Handle errors gracefully with error.tsx and not-found.tsx files.
Error Boundary
// app/dashboard/error.tsx
'use client' // Error components must be Client Components
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
Not Found
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>404 - Post Not Found</h2>
<p>Could not find the requested blog post.</p>
</div>
)
}
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
if (!post) {
notFound() // Triggers not-found.tsx
}
return <h1>{post.title}</h1>
}
Global Not Found
// app/not-found.tsx
export default function GlobalNotFound() {
return (
<div>
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
</div>
)
}
Data Fetching
Fetch data directly in Server Components using async/await.
Server Component Data Fetching
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store', // Disable caching (dynamic)
})
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Caching Strategies
// Static (cached by default)
fetch('https://api.example.com/posts')
// Revalidate every 60 seconds
fetch('https://api.example.com/posts', {
next: { revalidate: 60 }
})
// Dynamic (always fresh)
fetch('https://api.example.com/posts', {
cache: 'no-store'
})
// Force cache
fetch('https://api.example.com/posts', {
cache: 'force-cache'
})
Parallel Data Fetching
async function Page() {
// Fetch in parallel
const [user, posts] = await Promise.all([
fetch('https://api.example.com/user').then(r => r.json()),
fetch('https://api.example.com/posts').then(r => r.json()),
])
return (
<div>
<h1>{user.name}</h1>
<PostList posts={posts} />
</div>
)
}
Server Actions
Server Actions enable you to run server-side code directly from Client Components without API routes.
Basic Server Action
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title')
const content = formData.get('content')
// Save to database
await db.post.create({
data: { title, content }
})
revalidatePath('/posts')
}
// app/create/page.tsx
import { createPost } from '../actions'
export default function CreatePost() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required /></textarea>
<button type="submit">Create Post</button>
</form>
)
}
With Client Component
// app/actions.ts
'use server'
export async function incrementLikes(postId: string) {
await db.post.update({
where: { id: postId },
data: { likes: { increment: 1 } }
})
revalidatePath('/posts')
return { success: true }
}
// app/components/LikeButton.tsx
'use client'
import { incrementLikes } from '../actions'
import { useState } from 'react'
export default function LikeButton({ postId }: { postId: string }) {
const [isPending, setIsPending] = useState(false)
async function handleLike() {
setIsPending(true)
await incrementLikes(postId)
setIsPending(false)
}
return (
<button onClick={handleLike} disabled={isPending}>
{isPending ? 'Liking...' : 'Like'}
</button>
)
}
Server Action Benefits
- No API Routes: Write backend logic without creating endpoints
- Type Safety: Full TypeScript support end-to-end
- Progressive Enhancement: Forms work without JavaScript
- Security: Server code never sent to client
- Revalidation: Automatically update cached data