Claude Code und Next.js Full-Stack: praktischer App-Router-Leitfaden
Praxisguide für Claude Code mit Next.js App Router: Server/Client-Grenzen, Server Actions, APIs, Validierung, Auth und Review.
Claude Code kann eine Next.js-Full-Stack-Funktion sehr schnell erzeugen. Nützlich ist das aber nur, wenn die Grenzen im App Router klar sind. Bei einer vagen Anweisung kann serverseitige Logik in Browser-Komponenten landen, jede Seite wird unnötig zum Client Component, oder ungeprüftes JSON wird direkt gespeichert.
Dieser Leitfaden nutzt ein kleines authentifiziertes Task-Dashboard als Beispiel. Du siehst eine App-Router-Struktur, die Entscheidung zwischen Server Components und Client Components, Route Handlers, Server Actions, Validierung, Umgebungsvariablen, Auth-Grenzen und einen Review-Prompt für Claude Code.
Als offizielle Grundlage dienen Next.js App Router docs, Server and Client Components, Route Handlers, Mutating Data und der Backend for Frontend guide. Für den Agenten-Workflow siehe Claude Code common workflows.
Grenzen zuerst festlegen
Bei App Router ist nicht der Funktionsname das Schwierige, sondern der Ausführungsort. Ein Server Component wird auf dem Server gerendert. Ein Client Component läuft im Browser und ist nötig für State, Events oder Browser-APIs. Eine Server Action ist eine serverseitige Mutationsfunktion, oft aus einem Formular heraus. Ein Route Handler ist ein HTTP-Endpunkt für JSON-APIs, Webhooks oder externe Clients. BFF bedeutet Backend for Frontend: eine dünne Backend-Schicht für eine konkrete Oberfläche.
| Bereich | Einsatz | Darf enthalten | Anweisung an Claude Code |
|---|---|---|---|
| Server Component | Initiales Rendern, DB-Lesen, SEO-Seiten | DB-Zugriff, Auth, private APIs | Standardmäßig server-first lassen |
| Client Component | Formulare, Modals, Tabs, optimistische UI | useState, useActionState, Events | Keine Secrets, DB-Clients oder server-only Module |
| Server Action | Erstellen, Aktualisieren, Löschen aus der UI | Validierung, Auth, Mutation, Revalidation | Nicht als öffentliche API nutzen |
| Route Handler | Externe API, Webhook, Mobile Client | JSON, HTTP-Status, Signaturprüfung | Input validieren und Auth erzwingen |
Gib Claude Code diese Tabelle vor der Implementierung. Die wichtigste Regel lautet: keine Secrets in Client Components. Nur Variablen mit NEXT_PUBLIC_ sind für den Browser gedacht.
flowchart TD
Browser[Browser-Formular] --> Client[Client Component]
Client --> Action[Server Action]
External[Externer Dienst] --> Route[Route Handler]
Page[Server Component] --> Auth[Auth-Grenze]
Action --> Auth
Route --> Auth
Auth --> Data[DB oder Serverlogik]
Data --> Page
Projektstruktur
Bitte Claude Code zuerst um eine feste Dateistruktur. Im App Router ist das Dateisystem das Routing-Modell. Ohne Strukturvorgabe verteilt sich Logik schnell über app, components und lib.
src/
app/
dashboard/
tasks/
page.tsx
new/
page.tsx
actions.ts
api/
tasks/
route.ts
components/
task-create-form.tsx
lib/
auth.ts
env.ts
tasks.ts
page.tsx rendert die Startansicht, task-create-form.tsx enthält Browser-Interaktion, actions.ts verarbeitet UI-Mutationen, route.ts bietet HTTP an und lib enthält serverseitige Anwendungslogik.
Server-only Logik
Der folgende Speicher ist absichtlich nur in-memory, damit das Beispiel direkt kopierbar bleibt. In Produktion ersetzt du ihn durch Prisma, Drizzle, Supabase oder deine Datenzugriffsschicht. Entscheidend ist server-only, damit Client Components dieses Modul nicht versehentlich importieren.
// src/lib/tasks.ts
import "server-only";
export type TaskPriority = "low" | "normal" | "high";
export type Task = {
id: string;
ownerId: string;
title: string;
priority: TaskPriority;
dueDate: string | null;
createdAt: string;
};
const tasks: Task[] = [];
export async function listTasks(options: {
ownerId: string;
priority?: TaskPriority;
}) {
return tasks
.filter((task) => task.ownerId === options.ownerId)
.filter((task) => !options.priority || task.priority === options.priority)
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}
export async function createTask(input: {
ownerId: string;
title: string;
priority: TaskPriority;
dueDate?: string | null;
}) {
const task: Task = {
id: crypto.randomUUID(),
ownerId: input.ownerId,
title: input.title,
priority: input.priority,
dueDate: input.dueDate ?? null,
createdAt: new Date().toISOString(),
};
tasks.push(task);
return task;
}
Auch die Auth-Grenze bleibt serverseitig. Dieses Demo behandelt ein Cookie demo_user_id als eingeloggten Benutzer. In echten Produkten ersetzt du das durch Auth.js, Clerk oder deinen Identity Provider.
// src/lib/auth.ts
import "server-only";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export type CurrentUser = {
id: string;
name: string;
role: "member" | "admin";
};
export async function getCurrentUser(): Promise<CurrentUser | null> {
const cookieStore = await cookies();
const userId = cookieStore.get("demo_user_id")?.value;
if (!userId) {
return null;
}
return {
id: userId,
name: "Demo User",
role: "member",
};
}
export async function requireUser() {
const user = await getCurrentUser();
if (!user) {
redirect("/login");
}
return user;
}
export async function requireApiUser() {
return getCurrentUser();
}
Umgebungsvariablen sollten zentral validiert werden. Importiere diese Datei nie in einem Client Component.
// src/lib/env.ts
import "server-only";
import { z } from "zod";
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
APP_SECRET: z.string().min(32),
});
export const env = EnvSchema.parse(process.env);
Seite als Server Component
Die Task-Liste ist ein Server Component. Authentifizierte Daten werden auf dem Server gelesen, das Browser-Bundle bleibt kleiner und unnötige Client-Ladezustände entfallen.
// src/app/dashboard/tasks/page.tsx
import Link from "next/link";
import { requireUser } from "@/lib/auth";
import { listTasks } from "@/lib/tasks";
export default async function TasksPage() {
const user = await requireUser();
const tasks = await listTasks({ ownerId: user.id });
return (
<main className="mx-auto max-w-3xl space-y-6 p-6">
<div className="flex items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold">Tasks</h1>
<p className="text-sm text-gray-600">Work owned by {user.name}</p>
</div>
<Link className="rounded bg-black px-4 py-2 text-white" href="/dashboard/tasks/new">
New task
</Link>
</div>
<ul className="divide-y rounded border">
{tasks.map((task) => (
<li className="flex items-center justify-between p-4" key={task.id}>
<div>
<p className="font-medium">{task.title}</p>
<p className="text-sm text-gray-500">
Priority: {task.priority} / Due: {task.dueDate ?? "none"}
</p>
</div>
</li>
))}
</ul>
</main>
);
}
Sage Claude Code ausdrücklich, dass use client hier nur bei echter Browser-Interaktion erlaubt ist.
Mutation mit Server Action
Server Actions passen gut zu Formularmutationen. Auth, Validierung, Speichern und Revalidation bleiben in einer Funktion. Zod prüft die Eingabe, bevor Daten gespeichert werden.
// src/app/dashboard/tasks/new/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { requireUser } from "@/lib/auth";
import { createTask } from "@/lib/tasks";
const CreateTaskSchema = z.object({
title: z.string().trim().min(1, "Title is required").max(80),
priority: z.enum(["low", "normal", "high"]),
dueDate: z
.string()
.trim()
.optional()
.transform((value) => (value ? value : null)),
});
export type TaskFormState = {
ok: boolean;
message?: string;
fieldErrors?: Record<string, string[]>;
};
export async function createTaskAction(
previousState: TaskFormState,
formData: FormData
): Promise<TaskFormState> {
const user = await requireUser();
const parsed = CreateTaskSchema.safeParse({
title: formData.get("title"),
priority: formData.get("priority"),
dueDate: formData.get("dueDate"),
});
if (!parsed.success) {
return {
ok: false,
fieldErrors: parsed.error.flatten().fieldErrors,
message: "Please check the form fields.",
};
}
await createTask({
ownerId: user.id,
...parsed.data,
});
revalidatePath("/dashboard/tasks");
return {
ok: true,
message: "Task created.",
};
}
Das Client Component enthält nur Browser-State und Formularausgabe.
// src/components/task-create-form.tsx
"use client";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import {
createTaskAction,
type TaskFormState,
} from "@/app/dashboard/tasks/new/actions";
const initialState: TaskFormState = {
ok: false,
};
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
className="rounded bg-black px-4 py-2 text-white disabled:opacity-50"
disabled={pending}
type="submit"
>
{pending ? "Creating..." : "Create"}
</button>
);
}
export function TaskCreateForm() {
const [state, formAction] = useActionState(createTaskAction, initialState);
return (
<form action={formAction} className="space-y-4 rounded border p-4">
<div>
<label className="block text-sm font-medium" htmlFor="title">
Title
</label>
<input
className="mt-1 w-full rounded border px-3 py-2"
id="title"
name="title"
type="text"
/>
{state.fieldErrors?.title?.map((error) => (
<p className="mt-1 text-sm text-red-600" key={error}>
{error}
</p>
))}
</div>
<div>
<label className="block text-sm font-medium" htmlFor="priority">
Priority
</label>
<select className="mt-1 w-full rounded border px-3 py-2" id="priority" name="priority">
<option value="normal">Normal</option>
<option value="high">High</option>
<option value="low">Low</option>
</select>
</div>
<div>
<label className="block text-sm font-medium" htmlFor="dueDate">
Due date
</label>
<input className="mt-1 w-full rounded border px-3 py-2" id="dueDate" name="dueDate" type="date" />
</div>
{state.message ? <p className="text-sm text-gray-700">{state.message}</p> : null}
<SubmitButton />
</form>
);
}
// src/app/dashboard/tasks/new/page.tsx
import { TaskCreateForm } from "@/components/task-create-form";
export default function NewTaskPage() {
return (
<main className="mx-auto max-w-xl p-6">
<h1 className="mb-4 text-2xl font-bold">Create a task</h1>
<TaskCreateForm />
</main>
);
}
API mit Route Handler
Für externe Integrationen, Webhooks, mobile Clients oder JSON-APIs nutzt du Route Handlers. Server Actions sind bequem für UI-Mutationen, ersetzen aber keinen klaren HTTP-Vertrag.
// src/app/api/tasks/route.ts
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { requireApiUser } from "@/lib/auth";
import { createTask, listTasks } from "@/lib/tasks";
export const runtime = "nodejs";
const PrioritySchema = z.enum(["low", "normal", "high"]);
const CreateTaskApiSchema = z.object({
title: z.string().trim().min(1).max(80),
priority: PrioritySchema.default("normal"),
dueDate: z.string().date().nullable().optional(),
});
export async function GET(request: NextRequest) {
const user = await requireApiUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const priority = request.nextUrl.searchParams.get("priority");
const parsedPriority = priority ? PrioritySchema.safeParse(priority) : null;
if (parsedPriority && !parsedPriority.success) {
return NextResponse.json({ error: "Invalid priority" }, { status: 400 });
}
const tasks = await listTasks({
ownerId: user.id,
priority: parsedPriority?.data,
});
return NextResponse.json({ data: tasks });
}
export async function POST(request: NextRequest) {
const user = await requireApiUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json().catch(() => null);
const parsed = CreateTaskApiSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid request body", details: parsed.error.flatten().fieldErrors },
{ status: 400 }
);
}
const task = await createTask({
ownerId: user.id,
...parsed.data,
});
return NextResponse.json({ data: task }, { status: 201 });
}
Praktische Use Cases
Der erste Use Case ist ein internes Anfrage-Dashboard. Die Liste bleibt Server Component, das Erstellen läuft über Server Action, externe Benachrichtigungen über Route Handler.
Der zweite Use Case ist ein SaaS-Einstellungsbereich. Billing, Team-Einladungen und API-Key-Erstellung sollten eine kleine UI haben und Berechtigungen in Server Actions prüfen.
Der dritte Use Case ist ein CMS oder Produkt-Admin. Die erste Liste wird serverseitig gerendert, Uploads und Webhooks liegen in Route Handlers. Für Datenbankänderungen passt der Leitfaden zur DB-Migrationsautomatisierung.
Der vierte Use Case ist eine BFF-Architektur. Statt mehrere Drittanbieter-APIs direkt aus dem Browser aufzurufen, bündelst du sie hinter Next.js Route Handlers und hältst Secrets serverseitig.
Konkrete Fallen
Das größte Risiko ist eine verwischte Grenze. Prüfe, ob ein Client Component DB-Helfer importiert, ob initiales Laden unnötig in useEffect verschoben wurde, ob eine Server Action als öffentliche API dient oder ob ein Route Handler request.json() ohne Validierung speichert.
Auth muss serverseitig erzwungen werden. Ein versteckter Button ist keine Autorisierung. Server Actions und Route Handlers müssen Benutzer, Eigentümer und Rollen prüfen. Mehr dazu steht im Auth-Implementierungsleitfaden.
Vermeide außerdem vage Prompts wie “mach es sauber”. App-Router-Änderungen können Cache, Revalidation, Formularzustand und Auth betreffen. Gib Claude Code jedes Mal Scope, verbotene Änderungen und Prüfkommandos.
Review-Prompt für Claude Code
Nach der Implementierung solltest du zuerst nur Review anfordern, keine Änderungen.
You are reviewing a Next.js App Router full-stack change.
Scope:
- src/app/dashboard/tasks
- src/app/api/tasks/route.ts
- src/components/task-create-form.tsx
- src/lib/auth.ts
- src/lib/tasks.ts
- src/lib/env.ts
Check:
1. No secrets, DB clients, or server-only modules are imported by Client Components.
2. Server Components are not converted to Client Components without a real interaction need.
3. Server Actions validate input, check auth, mutate data, and revalidate the affected path.
4. Route Handlers return correct HTTP status codes and validate JSON bodies.
5. Auth is enforced on the server, not only hidden in the UI.
6. Tests or manual verification steps are listed for each risk.
Do not edit files yet. Return findings by severity with file paths and concrete fixes.
Korrigiere danach jeweils nur einen Befund. Mindestens prüfen: lint, typecheck, relevante Tests, manuelles Formular-Submit und ein nicht authentifizierter API-Aufruf, der 401 liefern muss. Für mehr Testtiefe siehe den Testing-Strategie-Leitfaden.
ClaudeCodeLab Training und Templates
Für ein Solo-Projekt reichen Struktur und Prompt aus diesem Artikel. In Teams sollten Grenzregeln, Review-Checklist, Auth-Regeln und Environment-Policy als Templates vorliegen.
ClaudeCodeLab bietet Claude Code training and templates und consultation for team adoption. Das hilft, Next.js-Admin-Oberflächen, CMS, SaaS-Einstellungen und Review-Loops an die eigenen Repository-Regeln anzupassen.
Zusammenfassung
Claude Code beschleunigt Next.js Full-Stack-Entwicklung, aber nur mit klarer Server-Browser-Grenze. Server Components für initiales Rendering, Client Components für Interaktion, Server Actions für UI-Mutationen, Route Handlers für externe APIs.
Beim Testen dieses Ablaufs war der größte Gewinn nicht mehr generierter Code, sondern weniger Review-Rauschen. Wenn Grenztabelle und Dateistruktur zuerst an Claude Code gingen, blieben meist UI-Texte und kleine Verhaltensthemen übrig. Bei einem vagen Anfangsprompt vermischten sich Formular- und API-Logik, und die Review dauerte länger als die Implementierung.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.