Next.js Server Components & App Router
The App Router (introduced in Next.js 13) brings a new paradigm with React Server Components, streaming, and simplified data fetching.
File-based Routing
// App Router structure
app/
layout.tsx → Root layout (required)
page.tsx → / (home page)
about/page.tsx → /about
blog/
page.tsx → /blog
[slug]/page.tsx → /blog/:slug
dashboard/
layout.tsx → Layout for /dashboard/*
page.tsx → /dashboard
settings/page.tsx → /dashboard/settings
api/
users/route.ts → /api/users (API route)
// app/page.tsx (home page)
export default function HomePage() {
return (
<div>
<h1>Welcome to Next.js</h1>
</div>
);
}
// app/blog/[slug]/page.tsx (dynamic route)
export default function BlogPost({ params }: { params: { slug: string } }) {
return (
<article>
<h1>Post: {params.slug}</h1>
</article>
);
}Server Components vs Client Components
Server Components (default) render on the server, reducing JavaScript sent to client. Client Components use "use client" directive and can use hooks, events, and browser APIs.
// Server Component (default) - no "use client"
export default async function ServerComponent() {
// Can access backend directly
const data = await fetch('https://api.example.com/data');
const posts = await data.json();
// Can import server-only code
import { db } from '@/lib/database';
const users = await db.users.findMany();
return (
<div>
<h1>Server Component</h1>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
// Client Component - needs "use client"
'use client';
import { useState, useEffect } from 'react';
export default function ClientComponent() {
const [count, setCount] = useState(0);
// Can use hooks, event handlers, browser APIs
useEffect(() => {
console.log('Count changed:', count);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// Composition: Server Component using Client Component
import Counter from './Counter'; // Client component
export default async function Page() {
const data = await fetchData(); // Server-side data fetch
return (
<div>
<h1>{data.title}</h1>
<Counter /> {/* Client component for interactivity */}
</div>
);
}Layouts
// app/layout.tsx (root layout - required)
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'My Next.js App',
description: 'Built with Next.js',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>
<nav>{/* Global nav */}</nav>
</header>
<main>{children}</main>
<footer>{/* Global footer */}</footer>
</body>
</html>
);
}
// app/dashboard/layout.tsx (nested layout)
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard-layout">
<aside>
<nav>{/* Dashboard nav */}</nav>
</aside>
<div className="content">
{children}
</div>
</div>
);
}Loading UI & Streaming
// app/dashboard/loading.tsx
export default function Loading() {
return <div>Loading dashboard...</div>;
}
// app/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>
);
}
// Streaming with Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading stats...</p>}>
<Stats /> {/* Async component */}
</Suspense>
<Suspense fallback={<p>Loading posts...</p>}>
<RecentPosts />
</Suspense>
</div>
);
}
async function Stats() {
const stats = await fetchStats();
return <div>{stats.total} users</div>;
}Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free