Server Components vs. Client Components
ServerWas im Server bleibt, was zum Client geht
# Prompt: "Erkläre die Server/Client-Grenze im Next.js App Router mit Beispielen"
// ✅ SERVER COMPONENT (Standard im App Router)
// app/products/page.tsx — kein 'use client' nötig
import { db } from '@/lib/db'
export default async function ProductsPage() {
// Direkt DB-Zugriff — kein API-Call nötig!
const products = await db.product.findMany({ where: { active: true } })
return (
<div>
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
)
}
// → Kein JS zum Client gesendet, kein Client-Side-Fetching, kein Loading-State
// ✅ CLIENT COMPONENT — nur wenn nötig
// components/AddToCart.tsx
'use client' // Directive MUSS erste Zeile sein
import { useState } from 'react'
export function AddToCart({ productId }: { productId: string }) {
const [loading, setLoading] = useState(false)
return <button onClick={() => addToCart(productId)}>In den Warenkorb</button>
}
// WANN Client Component?
// ✅ useState, useEffect, Event-Handler
// ✅ Browser-APIs (localStorage, window)
// ✅ Interaktive Widgets (Dropdown, Modal, Form)
// ❌ NICHT für Datenbankzugriffe, Secrets, schwere Logik
Claude Code Pattern-Prompt: "Analysiere diese Komponente: was gehört in Server Component, was braucht 'use client'? Trenne klar nach Server/Client-Grenze." Claude Code refaktoriert bestehende Pages-Router-Code präzise auf App Router.
Layouts und Verschachtelung
LayoutNested Layouts und Shared UI
# Prompt: "App Router Ordnerstruktur für SaaS mit Auth-Layout und Dashboard-Layout"
app/
├── layout.tsx # Root Layout (html, body, fonts)
├── page.tsx # Landing Page
├── (marketing)/ # Route Group — kein URL-Segment!
│ ├── about/page.tsx
│ └── blog/page.tsx
├── (auth)/ # Auth-Layout (kein Sidebar)
│ ├── layout.tsx # Nur zentrierter Card
│ ├── login/page.tsx
│ └── register/page.tsx
└── dashboard/ # Dashboard-Layout (mit Sidebar)
├── layout.tsx # Sidebar + Header
├── page.tsx # /dashboard
├── settings/
│ └── page.tsx # /dashboard/settings
└── [teamId]/ # Dynamische Route
└── page.tsx # /dashboard/[teamId]
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen">
<Sidebar /> {/* Server Component */}
<main className="flex-1 overflow-y-auto">
<Header />
{children} {/* Aktive Page */}
</main>
</div>
)
}
Loading, Error und Suspense
StatesLoading-UI und Error-Boundaries
# Prompt: "Loading und Error States für Dashboard-Page mit Streaming"
// app/dashboard/loading.tsx — automatisch als Suspense-Fallback
export default function DashboardLoading() {
return <DashboardSkeleton /> // Zeigt während Daten laden
}
// app/dashboard/error.tsx — Client Component Pflicht!
'use client'
export default function DashboardError({
error,
reset
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Fehler: {error.message}</h2>
<button onClick={reset}>Nochmal versuchen</button>
</div>
)
}
// Granulares Streaming mit Suspense
import { Suspense } from 'react'
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<StatsSkeleton />}>
<StatsSection /> {/* Streamt unabhängig */}
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders /> {/* Streamt unabhängig */}
</Suspense>
</div>
)
}
// → Beide Sections laden parallel, erscheinen sobald bereit
Server Actions: Formulare ohne API-Routes
ActionsServer Actions für Mutations
# Prompt: "Server Action für Profil-Update mit Validierung und Revalidation"
// app/dashboard/settings/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { auth } from '@/lib/auth'
export async function updateProfile(formData: FormData) {
const session = await auth()
if (!session) redirect('/login')
const name = formData.get('name') as string
const validated = profileSchema.parse({ name })
await db.user.update({
where: { id: session.userId },
data: validated
})
revalidatePath('/dashboard') // Cache invalidieren
// return { success: true } — oder redirect
}
// app/dashboard/settings/page.tsx — Formular direkt mit Action
export default function SettingsPage() {
return (
<form action={updateProfile}> {/* Server Action direkt! */}
<input name="name" />
<button type="submit">Speichern</button>
</form>
)
}
// → Kein fetch(), kein API-Route, kein useState — direkt Server-Aufruf
Route Handlers und Metadata API
# Prompt: "Route Handler für Webhook-Empfang und dynamische OG-Images"
// app/api/webhooks/stripe/route.ts
import { NextRequest } from 'next/server'
export async function POST(req: NextRequest) {
const body = await req.text()
const sig = req.headers.get('stripe-signature')!
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
switch (event.type) {
case 'checkout.session.completed':
await activateSubscription(event.data.object)
break
}
return Response.json({ received: true })
}
// Metadata API für SEO
// app/products/[slug]/page.tsx
export async function generateMetadata({ params }: Props) {
const product = await getProduct(params.slug)
return {
title: product.name,
description: product.description,
openGraph: {
title: product.name,
images: [{ url: product.imageUrl }]
}
}
}
// generateStaticParams für SSG
export async function generateStaticParams() {
const products = await db.product.findMany({ select: { slug: true } })
return products.map(p => ({ slug: p.slug }))
}
CachingData Fetching und Caching-Strategien
# Prompt: "Erkläre Next.js App Router Caching-Strategien für verschiedene Datentypen"
// Statisch gecacht (Build-time) — Default für Server Components
const config = await fetch('https://api.example.com/config')
// → Gebaut einmal, dann gecacht bis Redeploy
// Zeit-basierte Revalidierung
const posts = await fetch('https://api.blog.com/posts', {
next: { revalidate: 3600 } // 1 Stunde
})
// Kein Cache — immer frisch (dynamic)
const price = await fetch('https://api.stocks.com/price', {
cache: 'no-store'
})
// On-demand Revalidierung nach Mutation
import { revalidateTag } from 'next/cache'
const data = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
})
// Nach Update:
revalidateTag('posts') // Alle 'posts'-gecachten Requests ungültig
App Router vs Pages Router: getServerSideProps, getStaticProps und getStaticPaths sind im App Router ersetzt durch async Server Components + fetch-Caching. Claude Code migriert Pages-Router-Code automatisch auf die neuen Patterns.
Next.js-Modul im Kurs
Im Claude Code Mastery Kurs: vollständiges Next.js App Router-Modul mit Server Components, Server Actions, Layouts, Caching-Strategien, Route Handlers und Migration vom Pages Router — für moderne Full-Stack-Apps.
14 Tage kostenlos testen →