The App Router Revolution
The App Router represents the most significant architectural shift in Next.js history. Built on React Server Components, it fundamentally changes how we think about building web applications. Instead of the traditional client-heavy approach, the App Router defaults to server rendering with strategic client interactivity.
This isn't just a new file structure ā it's a new mental model. Pages are now server components by default. Layouts persist across navigations. Loading states are built into the routing system. And data fetching happens directly in your components, not in special lifecycle methods.
The Key Mental Shift
Stop thinking 'fetch data, then render.' Start thinking 'render, with data.' Server Components blur the line between component and API route. Your components can directly access databases, read files, and call external services ā all without shipping that code to the browser.
File-Based Routing
The App Router uses a file-system based router where folders define routes. Each folder represents a route segment that maps to a URL segment. Special files like page.tsx, layout.tsx, and loading.tsx have specific behaviors.
app/
āāā page.tsx # ā /
āāā layout.tsx # Root layout (required)
āāā loading.tsx # Loading UI for /
āāā error.tsx # Error UI for /
ā
āāā dashboard/
ā āāā page.tsx # ā /dashboard
ā āāā layout.tsx # Dashboard layout (nested)
ā āāā settings/
ā āāā page.tsx # ā /dashboard/settings
ā
āāā blog/
āāā page.tsx # ā /blog
āāā [slug]/
āāā page.tsx # ā /blog/:slug (dynamic)Layouts and Templates
Layouts are UI components that are shared between multiple pages. They preserve state, remain interactive, and don't re-render on navigation. This makes them perfect for navigation bars, sidebars, and any persistent UI elements.
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex min-h-screen">
<aside className="w-64 bg-gray-100 p-4">
<nav>
<Link href="/dashboard">Overview</Link>
<Link href="/dashboard/analytics">Analytics</Link>
<Link href="/dashboard/settings">Settings</Link>
</nav>
</aside>
<main className="flex-1 p-8">
{children}
</main>
</div>
);
}React Server Components
React Server Components (RSC) are components that render exclusively on the server. They never ship JavaScript to the client, can directly access backend resources, and automatically optimize for performance. In the App Router, all components are Server Components by default.
| Feature | Server Component | Client Component |
|---|---|---|
| Rendering | Server only | Server + Client |
| JavaScript sent to browser | None | Full component code |
| Can use hooks (useState, useEffect) | No | Yes |
| Can access backend resources directly | Yes | No (needs API) |
| Can use browser APIs | No | Yes |
| Interactivity | None | Full |
When to Use Each
- Use Server Components for: fetching data, accessing backend resources, keeping sensitive logic on the server, reducing client bundle size
- Use Client Components for: interactivity and event handlers, using React hooks, using browser APIs, third-party libraries that use hooks
// Server Component (default) - fetches data directly
async function PostList() {
const posts = await db.select().from(postsTable);
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// Client Component - needs 'use client' directive
'use client';
import { useState } from 'react';
function LikeButton({ postId }: { postId: string }) {
const [likes, setLikes] = useState(0);
return (
<button onClick={() => setLikes(l => l + 1)}>
ā¤ļø {likes}
</button>
);
}āThe best Next.js applications use Server Components by default and sprinkle in Client Components only where interactivity is needed. Think of client components as islands of interactivity in a sea of static content.ā
Data Fetching Patterns
The App Router introduces a radically simplified approach to data fetching. In Server Components, you can use async/await directly ā no useEffect, no loading states to manage, no client-side fetching libraries needed for initial data.
// Direct async/await in Server Components
async function BlogPage({ params }: { params: { slug: string } }) {
// This runs on the server - no client JS needed
const post = await db.query.posts.findFirst({
where: eq(posts.slug, params.slug),
with: {
author: true,
comments: true,
},
});
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<p>By {post.author.name}</p>
<div>{post.content}</div>
<CommentSection comments={post.comments} />
</article>
);
}Parallel Data Fetching
When you need to fetch multiple independent pieces of data, fetch them in parallel to minimize latency. Use Promise.all or leverage the natural parallelism of component composition.
// Parallel fetching with Promise.all
async function DashboardPage() {
// These requests happen in parallel
const [user, stats, notifications] = await Promise.all([
getUser(),
getStats(),
getNotifications(),
]);
return (
<div>
<UserProfile user={user} />
<StatsGrid stats={stats} />
<NotificationList notifications={notifications} />
</div>
);
}The Power of Composition
With Server Components, each component can independently fetch its own data. Parent components don't need to know what data their children need. This leads to more modular, maintainable code ā and automatic parallel fetching when components render simultaneously.