Next.js App Router Development Guide
This skill provides comprehensive guidance for building Next.js applications using the App Router (app directory).
Core Concepts
File-Based Routing
The App Router uses a file-system based router where:
-
Folders define routes
-
Files define UI for route segments
Special files:
File Purpose
page.tsx
Unique UI for a route (makes route publicly accessible)
layout.tsx
Shared UI for a segment and its children
loading.tsx
Loading UI (uses React Suspense)
error.tsx
Error UI (uses React Error Boundary)
not-found.tsx
Not found UI
route.ts
API endpoint (Route Handler)
template.tsx
Re-rendered layout
default.tsx
Fallback for parallel routes
Server Components vs Client Components
Server Components (Default)
-
All components in app/ are Server Components by default
-
Can directly access backend resources (database, file system)
-
Can use async/await at component level
-
Cannot use hooks, browser APIs, or event handlers
Client Components
-
Add 'use client' directive at the top of the file
-
Required for interactivity, hooks, browser APIs
-
Should be pushed down the component tree
// Server Component (default) export default async function Page() { const data = await fetchData() // Direct data fetching return <div>{data.title}</div> }
'use client'
// Client Component import { useState } from 'react'
export default function Counter() { const [count, setCount] = useState(0) return <button onClick={() => setCount(count + 1)}>{count}</button> }
Data Fetching Patterns
In Server Components (Recommended)
async function getData() { const res = await fetch('https://api.example.com/data', { cache: 'force-cache', // default - caches indefinitely // cache: 'no-store', // never cache // next: { revalidate: 3600 }, // revalidate every hour }) return res.json() }
export default async function Page() { const data = await getData() return <main>{data.content}</main> }
Server Actions (for mutations)
// app/actions.ts 'use server'
export async function createItem(formData: FormData) { const name = formData.get('name') await db.items.create({ data: { name } }) revalidatePath('/items') }
Routing Hooks (Client Components Only)
Import from next/navigation :
'use client'
import { useRouter, usePathname, useSearchParams, useParams } from 'next/navigation'
export function Navigation() { const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() const params = useParams()
// router.push('/dashboard') // router.replace('/login') // router.refresh() // router.back() // router.forward() }
Dynamic Routes
app/ ├── blog/ │ └── [slug]/ # Dynamic segment │ └── page.tsx ├── shop/ │ └── [...slug]/ # Catch-all segment │ └── page.tsx └── docs/ └── [[...slug]]/ # Optional catch-all └── page.tsx
// app/blog/[slug]/page.tsx export default async function Page({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params return <article>Post: {slug}</article> }
// Generate static paths export async function generateStaticParams() { const posts = await getPosts() return posts.map((post) => ({ slug: post.slug })) }
Layouts and Templates
Root Layout (Required)
// app/layout.tsx export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> ) }
Nested Layouts
// app/dashboard/layout.tsx export default function DashboardLayout({ children, }: { children: React.ReactNode }) { return ( <section> <nav>Dashboard Nav</nav> {children} </section> ) }
Loading and Error States
// app/dashboard/loading.tsx export default function Loading() { return <div>Loading...</div> }
// app/dashboard/error.tsx 'use client'
export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( <div> <h2>Something went wrong!</h2> <button onClick={() => reset()}>Try again</button> </div> ) }
Route Handlers (API Routes)
// app/api/items/route.ts import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams const query = searchParams.get('query')
const items = await getItems(query) return NextResponse.json(items) }
export async function POST(request: NextRequest) { const body = await request.json() const item = await createItem(body) return NextResponse.json(item, { status: 201 }) }
Metadata
// Static metadata export const metadata = { title: 'My Page', description: 'Page description', }
// Dynamic metadata export async function generateMetadata({ params }) { const post = await getPost(params.slug) return { title: post.title, description: post.excerpt, } }
Best Practices
-
Keep Client Components small - Push 'use client' as far down the tree as possible
-
Colocate data fetching - Fetch data in Server Components close to where it's used
-
Use Server Actions for mutations instead of API routes when possible
-
Leverage caching - Use appropriate fetch cache options
-
Handle loading/error states - Always provide loading.tsx and error.tsx
-
Use route groups (folder) for organization without affecting URL
-
Implement parallel routes @folder for complex layouts
-
Use intercepting routes (.)folder for modals
Common Patterns
See the references/ directory for detailed patterns:
-
routing.md
-
Advanced routing patterns
-
server-components.md
-
Server component patterns
-
data-fetching.md
-
Data fetching strategies
See the examples/ directory for working code:
-
page-layout.tsx
-
Page and layout examples
-
server-action.ts
-
Server action examples
-
route-handler.ts
-
API route examples