TL;DR
- File-system routing.
app/page.tsx = /. app/blog/page.tsx = /blog.
- Special files:
layout.tsx, loading.tsx, error.tsx, not-found.tsx.
- Server Components by default. Add
"use client" only when needed.
Route Structure
app/
├── page.tsx → /
├── layout.tsx → wraps all pages
├── loading.tsx → loading UI (Suspense)
├── error.tsx → error boundary
├── not-found.tsx → 404
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/:slug
└── (marketing)/ → route group (no URL segment)
├── about/page.tsx → /about
└── pricing/page.tsx → /pricing
Dynamic Routes
// app/blog/[slug]/page.tsx
interface Props {
params: Promise<{ slug: string }>;
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}
// Static generation
export function generateStaticParams() {
return posts.map(p => ({ slug: p.slug }));
}
Server vs Client
| Feature |
Server Component |
Client Component |
| Default? |
Yes |
Need "use client" |
| Can fetch data |
Yes (async/await) |
useEffect only |
| Can use hooks |
No |
Yes |
| Can access DB |
Yes |
No |
| Bundle size |
Zero |
Adds to bundle |
| Interactivity |
No |
Yes |
Data Fetching
// Server Component — just await
export default async function Page() {
const data = await fetch('https://api.example.com/posts');
const posts = await data.json();
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}