The App Router, introduced in Next.js 13 and stabilized in 14, is a fundamental rethink of how you build Next.js apps. It embraces React Server Components, co-locates layouts with routes, and gives you granular control over streaming, caching, and data fetching.
The File-System Convention
Every folder inside app/ can contain a handful of special files. The ones you'll use most:
page.tsx— the UI for a route segment. Required for the route to be publicly accessible.layout.tsx— shared UI that wraps child segments. Persists across navigations.loading.tsx— React Suspense fallback shown while the page streams in.error.tsx— error boundary for the segment and its children.not-found.tsx— rendered whennotFound()is called.
Server vs Client Components
By default, every component in app/ is a React Server Component (RSC). RSCs run on the server, can directly await database queries and secrets, and send zero JavaScript to the browser.
// app/posts/page.tsx — a Server Component
// No "use client" → runs on server, can use async/await
import { db } from "@/lib/db"
export default async function PostsPage() {
const posts = await db.post.findMany({ orderBy: { createdAt: "desc" } })
return (
<ul>
{posts.map((p) => <li key={p.id}>{p.title}</li>)}
</ul>
)
}Note: Add
"use client"only when you need browser APIs, event handlers, or React hooks likeuseState/useEffect. Keep client boundaries as far down the tree as possible.
Nested Layouts
Layouts wrap their children and persist across navigations within their segment. You can nest layouts to compose complex UIs without re-rendering shared chrome:
app/
layout.tsx # Root layout — wraps everything
dashboard/
layout.tsx # Dashboard sidebar + header
page.tsx # /dashboard
settings/
page.tsx # /dashboard/settings (uses dashboard layout)Streaming with Loading UI
Place a loading.tsx next to your page.tsx and Next.js automatically wraps the page in a Suspense boundary. The layout renders immediately while the page's async work completes:
// app/posts/loading.tsx
export default function Loading() {
return (
<div className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="h-16 rounded-lg bg-muted animate-pulse" />
))}
</div>
)
}Data Fetching Patterns
- Fetch in Server Components — keeps secrets server-side, eliminates client waterfalls
- Use parallel fetching with
Promise.all()for independent queries - Leverage Next.js extended
fetch()with{ cache: 'force-cache' | 'no-store' }or{ next: { revalidate: 60 } } - Use React
cache()to deduplicate identical requests within a render
// Parallel fetching pattern
export default async function Page({ params }: { params: { id: string } }) {
const [post, author] = await Promise.all([
getPost(params.id),
getAuthor(params.id),
])
return <PostView post={post} author={author} />
}Warning: Avoid sequential
awaits for independent data. Each await in a chain blocks the next — usePromise.all()to run them concurrently.
Dynamic vs Static Routes
By default, routes are statically rendered at build time. Opt into dynamic rendering by using dynamic data sources (cookies, headers, searchParams) or exporting dynamic = 'force-dynamic'. For the best performance, prefer static and revalidate on demand with revalidatePath() or revalidateTag().