Next.js 16 App Router: Essential Patterns and Best Practices
Next.js 16 brings significant improvements to the App Router, making it more powerful and easier to use. This guide covers the essential patterns you need to know.
Server vs Client Components
Understanding when to use Server vs Client Components is crucial:
Server Components (Default)
Use Server Components when you:
- Fetch data from APIs or databases
- Access backend resources directly
- Keep sensitive information on the server
- Need SEO optimization
// app/posts/page.tsx - Server Component
export default async function PostsPage() {
const posts = await getPosts() // Direct database access
return <PostList posts={posts} />
}
Client Components
Use Client Components when you need:
- Interactivity (onClick, onChange, etc.)
- State management (useState, useReducer)
- Browser APIs (localStorage, window, etc.)
- Effects (useEffect)
"use client"
export function SearchBar() {
const [query, setQuery] = useState('')
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
)
}
Routing Patterns
Dynamic Routes
Create dynamic routes with folder brackets:
app/
├── posts/
│ └── [slug]/
│ └── page.tsx
// Generate static params at build time
export async function generateStaticParams() {
const slugs = await getAllPostSlugs()
return slugs.map((slug) => ({ slug }))
}
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPostBySlug(slug)
return <Article post={post} />
}
Route Groups
Organize routes without affecting URLs:
app/
├── (marketing)/
│ ├── about/
│ └── contact/
├── (app)/
│ ├── dashboard/
│ └── settings/
Data Fetching
Async Server Components
Fetch data directly in Server Components:
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache' // SSG
})
return res.json()
}
export default async function Page() {
const data = await getData()
return <div>{data.title}</div>
}
Revalidation Strategies
Control data freshness:
// Revalidate every 60 seconds
export const revalidate = 60
// Or per-fetch
fetch('https://api.example.com/data', {
next: { revalidate: 3600 }
})
Metadata API
Dynamic metadata for SEO:
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
images: [post.coverImage],
},
}
}
Loading & Error States
Loading UI
Create loading states with loading.tsx:
// app/posts/loading.tsx
export default function Loading() {
return <Skeleton />
}
Error Handling
Handle errors gracefully with error.tsx:
// app/posts/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
)
}
Streaming with Suspense
Progressive rendering for better UX:
import { Suspense } from 'react'
export default function Page() {
return (
<>
<Header />
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</>
)
}
Performance Tips
- Minimize Client Components: Only mark as
"use client"when necessary - Use Static Generation: Pre-render pages at build time when possible
- Implement Streaming: Show content progressively with Suspense
- Optimize Images: Always use
next/imagecomponent - Enable Caching: Configure appropriate cache strategies
Common Patterns
Layout Composition
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Navbar />
{children}
<Footer />
</body>
</html>
)
}
Parallel Routes
Load multiple segments simultaneously:
app/
├── @team/
│ └── page.tsx
├── @analytics/
│ └── page.tsx
└── page.tsx
Conclusion
The Next.js 16 App Router provides powerful tools for building modern web applications. Key principles:
- Server Components by default for better performance
- Client Components only when interactivity is needed
- Leverage static generation whenever possible
- Use Suspense for progressive rendering
Master these patterns, and you'll build faster, more maintainable Next.js applications.
Have questions or tips to share? Let's discuss in the comments!