Server Actions: Formularverarbeitung ohne separate API
1. Übersicht
Früher musste man, um ein Formular in Next.js zu verarbeiten, eine API-Route (app/api/.../route.ts) schreiben und dann fetch vom Client aus aufrufen, um Daten einzureichen. Server Actions eliminieren diesen Mittelstufen-Schritt: Sie schreiben einfach eine Funktion, die auf dem Server läuft, hängen sie direkt an das action-Attribut des <form> an, und fertig.
Wenn Sie diesen Artikel lesen, lernen Sie, wie Sie Server Actions deklarieren, sie für Formulare mit Validierung verwenden, den Status von pending/error mit useFormState und useFormStatus verwalten, Daten nach dem Absenden revalidieren und häufige Fehler beim Wechsel von API Routes vermeiden.
Hinweis: Server Actions sind eine Funktion des App Router (Next.js 13.4+) und stabil seit Next.js 14. Wenn Sie noch den Pages Router verwenden, ist dieser Artikel nicht anwendbar.
2. Ihre erste Server Action
Eine Server Action ist eine asynchrone Funktion mit der Direktive "use server", die entweder in einer separaten Datei oder direkt in einer Komponente (wenn es eine Server-Komponente ist) platziert werden kann.
// app/actions/customer-actions.ts
"use server";
export async function createCustomer(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
console.log("Kunde erstellen:", name, email);
// Simuliere DB-Speicherung
}
// app/customers/new/page.tsx
import { createCustomer } from "@/app/actions/customer-actions";
export default function NewCustomerPage() {
return (
<form action={createCustomer}>
<input name="name" placeholder="Kundenname" />
<input name="email" placeholder="Email" />
<button type="submit">Erstellen</button>
</form>
);
}
"use server"am Anfang der Datei teilt Next.js mit, dass alle Exporte in dieser Datei Server Actions sind, die nur auf dem Server laufen und nicht in das JavaScript gebündelt werden, das an den Client gesendet wird.action={createCustomer}bindet die Funktion direkt an das Formular an. Next.js serialisiert das Formular automatisch zuFormDataund ruft die Funktion auf dem Server auf, wenn es eingereicht wird.- Sie benötigen kein
onSubmit, keinpreventDefaultund keinfetch. Das Formular funktioniert auch, wenn JavaScript noch nicht vollständig geladen ist (progressive Enhancement).
Das Ergebnis beim Absenden: Das Log wird im Server-Terminal angezeigt (nicht in der Browser-Konsole), da die Funktion auf dem Server ausgeführt wird.
3. Ergebnisse mit useFormState an den Client zurückgeben
In der Praxis müssen Sie Validierungsfehler oder Erfolgsmeldungen anzeigen. useFormState (aus react-dom) hilft, dass Server Actions State an den Client zurückgeben.
// app/actions/customer-actions.ts
"use server";
type ActionResult = { success: boolean; message: string };
export async function createCustomer(
_prevState: ActionResult,
formData: FormData
): Promise<ActionResult> {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
if (!name || !email) {
return { success: false, message: "Name oder Email fehlt." };
}
if (!email.includes("@")) {
return { success: false, message: "E-Mail ist ungültig." };
}
// Simuliere DB-Speicherung
return { success: true, message: `Kunde ${name} wurde erstellt.` };
}
// app/customers/new/page.tsx
"use client";
import { useFormState } from "react-dom";
import { createCustomer } from "@/app/actions/customer-actions";
const initialState = { success: false, message: "" };
export default function NewCustomerForm() {
const [state, formAction] = useFormState(createCustomer, initialState);
return (
<form action={formAction}>
<input name="name" placeholder="Kundenname" />
<input name="email" placeholder="Email" />
<button type="submit">Erstellen</button>
{state.message && (
<p style={{ color: state.success ? "green" : "red" }}>
{state.message}
</p>
)}
</form>
);
}
useFormState(action, initialState)wrapped die Server Action und gibt[state, formAction]zurück.stateist der letzte Rückgabewert der Action,formActionist die Funktion, die an<form action={...}>gebunden wird.- Die Server Action erhält
prevStateals ersten zusätzlichen Parameter — dies ist obligatorisch bei Verwendung mituseFormState, auch wenn Sie es nicht verwenden. - Da
useFormStateein React Hook ist, muss diese Datei eine Client-Komponente sein, daher benötigen Sie"use client".
# E-Mail fehlt
> Name oder Email fehlt.
# Gültige Einreichung
> Kunde Nguyễn Văn A wurde erstellt.
4. Pending-Status mit useFormStatus
Beim Absenden möchten Sie den Button deaktivieren und eine Ladeanimation anzeigen. useFormStatus liest den Pending-Status des nächsten übergeordneten Formulars — aber es muss sich in einer separaten untergeordneten Komponente befinden.
// app/customers/new/submit-button.tsx
"use client";
import { useFormStatus } from "react-dom";
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Wird erstellt..." : "Erstellen"}
</button>
);
}
useFormStatusfunktioniert nur, wenn die Komponente innerhalb eines<form>gerendert wird. Wenn Sie<SubmitButton />in derselben Komponente, die das<form>enthält, platzieren, funktioniert es trotzdem korrekt, da React das nächste übergeordnete Formular im DOM-Baum sucht, nicht in derselben Komponente.pendingisttrue, während die Server Action auf dem Server ausgeführt wird, wodurch Sie den Button deaktivieren können, um doppelte Einreichungen zu vermeiden.
Hinweis: Rufen Sie
useFormStatusnicht direkt in der Komponente auf, die das<form>enthält, und erwarten Sie, dass es den Status kennt — es funktioniert zwar, weil es den DOM-Baum durchsucht, aber es ist Best Practice, es als separate untergeordnete Komponente zu extrahieren, um Verwirrung zu vermeiden, wenn mehrere Formulare auf derselben Seite vorhanden sind.
5. Datenvalidierung mit Zod
Manuelle Validierung mit if/else wird schnell verwirrend. Verwenden Sie Zod, um Schemas klar zu deklarieren.
<!-- Nicht anwendbar — dies ist eine npm-Abhängigkeit, siehe unten -->
npm install zod
// app/actions/customer-actions.ts
"use server";
import { z } from "zod";
const customerSchema = z.object({
name: z.string().min(2, "Name muss mindestens 2 Zeichen haben."),
email: z.string().email("E-Mail ist ungültig."),
});
type ActionResult = {
success: boolean;
message: string;
errors?: Record<string, string[]>;
};
export async function createCustomer(
_prevState: ActionResult,
formData: FormData
): Promise<ActionResult> {
const parsed = customerSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
});
if (!parsed.success) {
return {
success: false,
message: "Daten sind ungültig.",
errors: parsed.error.flatten().fieldErrors,
};
}
return { success: true, message: `Kunde ${parsed.data.name} wurde erstellt.` };
}
safeParsegibt ein Objekt mitsuccessunddata/errorzurück, wirft keine Exception — es ist geeignet, um Fehler an den Client zurückzugeben, anstatt die Action abstürzen zu lassen.flatten().fieldErrorsgibt ein Objekt der Form{ name: ["Name muss..."], email: [...] }zurück, praktisch um Fehler direkt unter jedem Input anzuzeigen.- Das Schema wird einmal definiert und kann für Client- (falls nötig) und Server-Validierung wiederverwendet werden — vermeiden Sie wiederholte Logik.
6. Daten nach dem Absenden revalidieren
Nachdem Sie einen Kunden erstellt haben, muss die Kundenlisten-Seite (die von Next.js zwischengespeichert wird) aktualisiert werden. Verwenden Sie revalidatePath oder revalidateTag.
// app/actions/customer-actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createCustomer(formData: FormData) {
const name = formData.get("name") as string;
// ... in DB speichern
revalidatePath("/customers");
redirect("/customers");
}
revalidatePath("/customers")löscht den Cache dieser Seite, beim nächsten Zugriff wird Next.js sie mit neuen Daten neu rendern.redirectwirft eine spezielle Exception, die Next.js fängt, um weiterzuleiten — rufen Sie sie nachrevalidatePathauf, und legen Sie sie nicht intry/catch, da dies die Weiterleitung blockiert.- Beide Funktionen können nur in einer Server Action oder Server-Komponente aufgerufen werden, nicht vom Client.
7. Server Actions aus Client-Komponenten aufrufen (nicht über Formular)
Server Actions müssen nicht an ein <form> gebunden sein. Sie können sie direkt wie eine asynchrone Funktion aufrufen, z. B. in onClick.
// app/customers/[id]/delete-button.tsx
"use client";
import { useState } from "react";
import { deleteCustomer } from "@/app/actions/customer-actions";
export function DeleteButton({ customerId }: { customerId: string }) {
const [isDeleting, setIsDeleting] = useState(false);
async function handleClick() {
setIsDeleting(true);
await deleteCustomer(customerId);
setIsDeleting(false);
}
return (
<button onClick={handleClick} disabled={isDeleting}>
{isDeleting ? "Wird gelöscht..." : "Löschen"}
</button>
);
}
// app/actions/customer-actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function deleteCustomer(id: string) {
console.log("Kunde mit ID löschen:", id);
revalidatePath("/customers");
}
- Eine Server Action wird von Next.js tatsächlich in einen versteckten POST-Endpoint kompiliert, den der Client über eine automatisch erstellte Netzwerkanfrage aufruft — Sie sehen keinen Fetch-Code, aber es ist real und sichtbar im Network-Tab.
- Dieser Ansatz ist praktisch für Löschen, Like und Status-Toggle-Buttons — wo Sie kein vollständiges
<form>benötigen.
8. Server Actions testen
Da eine Server Action nur eine asynchrone Funktion ist, die FormData empfängt, können Sie sie wie eine normale Unit-Funktion testen, ohne HTTP zu mocken.
npm install -D vitest
// app/actions/customer-actions.test.ts
import { describe, it, expect } from "vitest";
import { createCustomer } from "./customer-actions";
function buildFormData(data: Record<string, string>): FormData {
const fd = new FormData();
for (const [key, value] of Object.entries(data)) {
fd.append(key, value);
}
return fd;
}
describe("createCustomer", () => {
it("gibt einen Fehler zurück, wenn E-Mail ungültig ist", async () => {
const formData = buildFormData({ name: "An", email: "khong-hop-le" });
const result = await createCustomer({ success: false, message: "" }, formData);
expect(result.success).toBe(false);
expect(result.errors?.email).toContain("E-Mail ist ungültig.");
});
it("erfolgreich mit gültigen Daten", async () => {
const formData = buildFormData({ name: "An", email: "an@example.com" });
const result = await createCustomer({ success: false, message: "" }, formData);
expect(result.success).toBe(true);
expect(result.message).toContain("An");
});
});
buildFormDatasimuliertFormDatawie vom Browser gesendet, ohne Komponenten zu rendern oder den Server zu mocken.- Da eine Server Action eine reine Funktion ist (nimmt Input entgegen, gibt Output zurück), laufen Tests schnell, ohne einen Next.js Dev-Server zu starten.
Hinweis: Wenn die Server Action
redirectoderrevalidatePathaufruft, wirft der Test einen Fehler, da diese Funktionen nur im echten Request-Kontext von Next.js funktionieren. Trennen Sie Business-Logik (Validierung, Berechnungen) von denredirect/revalidatePath-Aufrufen ab, um Tests zu erleichtern.
9. Häufige Fehler
Vergessen von "use server" oder falsche Platzierung. Die Direktive muss am Anfang der Datei (oder am Anfang der Funktion, wenn inline in einer Server-Komponente deklariert) stehen. Ohne sie behandelt Next.js die Funktion als normalen Code, bündelt sie mit dem Client und es gibt einen Fehler zur Laufzeit.
Verwendung von useFormState/useFormStatus in Server-Komponenten. Diese beiden Hooks gehören zu react-dom und laufen nur in Client-Komponenten. Das Vergessen von "use client" verursacht einen Build-Fehler.
Rückgabe komplexer Objekte (Klasseninstanzen, nicht serialisierte Datum) aus Server Actions. Die zurückgegebenen Daten müssen über das Netzwerk serialisierbar sein wie JSON. Rückgabe von rohen Date verursacht manchmal stille Fehler — konvertieren Sie zuerst zu String.
Server Actions für alles aufrufen, auch für Logik, die den Server nicht benötigt. Wenn es nur einfache Validierung ohne Datenbankzugriff ist, ist die Verarbeitung auf dem Client schneller und vermeidet unnötige Round-Trips.
Kein Fallback bei JavaScript-Fehlern. Server Actions unterstützen progressive Enhancement, aber nur, wenn Sie korrekt <form action={...}> verwenden. Wenn Sie preventDefault() aufrufen und die Action manuell aufrufen, verlieren Sie diesen Vorteil.
10. Wann Sie Server Actions nicht verwenden sollten
Server Actions sind gut für interne CRUD-Formulare Ihrer Next.js-App. Aber wenn Sie benötigen:
- Eine öffentliche API für Mobile-Apps oder Drittanbieter zum Aufrufen — schreiben Sie einen echten Route Handler (
route.ts) mit klarer Versionierung und Authentifizierung. - Streaming-Antworten oder Verarbeitung sehr großer Datei-Uploads mit detaillierter Kontrolle — Route Handler sind flexibler.
- Logik, die unabhängig ausgeführt werden oder außerhalb des Next.js-Kontexts wiederverwendet werden muss (z. B. Worker, Cron-Job) — trennen Sie sie als separaten Service, Server Action ruft nur den Service auf.
11. Fazit
Server Actions helfen Ihnen, Formulare zur Verarbeitung von Daten auf dem Server zu schreiben, ohne separate API-Routes zu erstellen, wodurch viel Fetch/JSON-Boilerplate gespart wird. Kombinieren Sie sie mit useFormState zur Fehleranzeige, useFormStatus für Pending-Anzeige und revalidatePath zur Cache-Aktualisierung, und Sie haben einen sauberen CRUD-Workflow, der auch funktioniert, wenn JavaScript noch nicht geladen ist. Verwenden Sie Zod für klare Validierung und denken Sie daran, Actions in echte Route Handler zu trennen, wenn Sie öffentliche APIs für externe Aufrufer benötigen.