Deep dive into Next.js 13+ App Router, Server Components, and the new paradigm for building web applications.
The App Router represents a paradigm shift in how we build Next.js applications. Let's explore the key concepts and patterns.
The App Router introduces React Server Components, streaming, and new conventions for routing, layouts, and data fetching. It's the default for new Next.js applications.
Server Components render on the server and send HTML to the client. They're the default in the App Router:
// app/dashboard/page.tsx - Server Component by default
import { db } from "@/lib/db";
import { Suspense } from "react";
async function getUserData(userId: string) {
// Direct database access - no API needed!
return db.user.findUnique({ where: { id: userId } });
}
async function DashboardStats() {
const stats = await db.stats.findMany();
return (
<div className="grid gap-4 md:grid-cols-3">
{stats.map((stat) => (
<div key={stat.id} className="p-4 border rounded">
<h3>{stat.name}</h3>
<p className="text-2xl">{stat.value}</p>
</div>
))}
</div>
);
}
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading stats...</div>}>
<DashboardStats />
</Suspense>
</div>
);
}
Server Components can't use hooks (useState, useEffect), event handlers (onClick), or browser APIs. Use Client Components for interactivity.
Client Components render in the browser and support interactivity. Add "use client" at the top of the file:
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<Button onClick={() => setCount(c => c + 1)}>Increment</Button>
</div>
);
}
Use Server Components for:
Use Client Components for:
The App Router uses a file-based routing system with colocation:
app/
āāā (dashboard)/ # Route group (doesn't affect URL)
ā āāā layout.tsx # Shared layout for dashboard routes
ā āāā page.tsx # /dashboard
ā āāā settings/
ā ā āāā page.tsx # /dashboard/settings
ā āāā [id]/ # Dynamic segment
ā āāā page.tsx # /dashboard/123
āāā blog/
ā āāā page.tsx # /blog
ā āāā [slug]/ # Dynamic route
ā ā āāā page.tsx # /blog/my-post
ā āāā tag/
ā āāā [tag]/ # Catch-all segment
ā āāā page.tsx # /blog/tag/nextjs
āāā layout.tsx # Root layout (required)
āāā page.tsx # / (homepage)
āāā loading.tsx # Loading UI for root
āāā error.tsx # Error boundary for root
āāā not-found.tsx # 404 page for root
Use parentheses (name) for route groups - they organize files without affecting the URL:
// app/(dashboard)/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<Sidebar />
<main>{children}</main>
</div>
);
}
Use square brackets [param] for dynamic segments:
// app/blog/[slug]/page.tsx
async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetchBlogPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
| Pattern | URL | Example |
|---|---|---|
page.tsx | / | Homepage |
about/page.tsx | /about | Static route |
[id]/page.tsx | /123 | Dynamic route |
[...slug]/page.tsx | /a/b/c | Catch-all route |
[[...slug]]/page.tsx | / or /a/b/c | Optional catch-all |
Server Components can be async, making data fetching simple:
// app/users/page.tsx
async function UserList() {
const users = await fetch("https://api.example.com/users").then(r => r.json());
return (
<ul>
{users.map((user: User) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default function UsersPage() {
return (
<div>
<h1>Users</h1>
<UserList />
</div>
);
}
Control caching with fetch options:
// Static data (cached indefinitely)
const data = await fetch("https://api.example.com/data", {
cache: "force-cache",
});
// No caching (always fresh)
const data = await fetch("https://api.example.com/data", {
cache: "no-store",
});
// Revalidate every 60 seconds
const data = await fetch("https://api.example.com/data", {
next: { revalidate: 60 },
});
// Revalidate on demand by tag
const data = await fetch("https://api.example.com/data", {
next: { tags: ["posts"] },
});
Every App Router application needs a root layout:
// app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "My App",
description: "Built with Next.js",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
Layouts nest based on the file structure:
// app/(dashboard)/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<Sidebar />
<main>{children}</main>
</div>
);
}
// app/(dashboard)/settings/layout.tsx
export default function SettingsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<h1>Settings</h1>
<Tabs>{children}</Tabs>
</div>
);
}
The settings page will have both layouts applied!
Layouts persist across navigations and preserve state. Templates create a new instance for each navigation, useful for animations or re-fetching data.
// layout.tsx - Preserves state
export default function Layout({ children }) {
return <div>{children}</div>;
}
// template.tsx - Re-mounts on navigation
export default function Template({ children }) {
return <div>{children}</div>;
}
Create a loading.tsx file to show loading state automatically:
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="space-y-4">
<div className="h-8 w-48 animate-pulse bg-muted rounded" />
<div className="grid gap-4 md:grid-cols-3">
{[1, 2, 3].map((i) => (
<div key={i} className="h-32 animate-pulse bg-muted rounded" />
))}
</div>
</div>
);
}
Create an error.tsx file to handle errors:
// app/dashboard/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
The App Router provides a more intuitive way to build Next.js apps with better performance and developer experience. Start using Server Components by default, and opt into Client Components when needed.