Design wechseln

Next.js Server Actions Tutorial: Formularverarbeitung und Validierung – Best Practices

Easton editorial illustration: API gateway workstation

Am Rechner sitzen, Code für ein Registrierungsformular ansehen. Im Ordner liegen schon vier Dateien: Formular-Komponente, API Route, Typdefinitionen, Fehlerbehandlung … Für ein simples Formular-Submit sind fast 200 Zeilen zusammengekommen.

Gibt es keinen einfacheren Weg?

Die Antwort: Server Actions. Dieses Feature im Next.js App Router kann die Formularverarbeitung um bis zu 80 % vereinfachen. Keine API Route, kein manuelles fetch, kein umständliches State-Management. Klingt gut – aber Sie fragen sich vielleicht: Ist das wirklich sicher? Wie validiere ich? Wie handle ich Loading?

Am Anfang hatte ich dieselben Bedenken. Nach einigen Monaten Nutzung – mit ein paar Stolpersteinen – möchte ich die Praxis-Tipps für Next.js Server Actions bei Formularen teilen: vom einfachen Submit über Zod-Validierung und Sicherheit bis zur UX-Optimierung. Mit echten Codebeispielen, damit Sie schnell einsteigen.

Server Actions – Grundlagen

Was sind Server Actions?

Server Actions sind asynchrone Funktionen, die auf dem Server laufen. Sie markieren sie mit 'use server' und können sie direkt in der action-Eigenschaft eines Formulars verwenden. Beim Submit wird die Funktion automatisch aufgerufen – Datenverarbeitung, Datenbankoperationen, Cache-Updates: alles passiert serverseitig.

Die wichtigsten Eigenschaften:

  • Typsicherheit: TypeScript prüft die gesamte Kette
  • Zero Configuration: kein /api-Ordner nötig
  • Automatische Verarbeitung: FormData wird automatisch übergeben

Zwei Schreibweisen: Server Action direkt in der Komponente (inline) oder in einer separaten Datei (Modul-Ebene):

// Variante 1: inline in der Komponente
export default function Page() {
  async function createUser(formData: FormData) {
    'use server' // als Server Action markieren
    const name = formData.get('name')
    // Daten verarbeiten...
  }

  return <form action={createUser}>...</form>
}

// Variante 2: separate Datei (empfohlen)
// app/actions.ts
'use server' // Markierung auf Datei-Ebene

export async function createUser(formData: FormData) {
  const name = formData.get('name')
  // Daten verarbeiten...
}

Die Frage: Was unterscheidet Server Actions von klassischen API Routes – und wann nutzt man welche?

Hier eine Vergleichstabelle:

EigenschaftServer ActionsAPI Routes
EinsatzFormular-Submit, DatenänderungenRESTful API, externe Aufrufe
HTTP-Methodenur POSTGET/POST/PUT/DELETE usw.
Typsicherheitnativ typsicherTypen manuell definieren
AufrufFunktion direkt aufrufenfetch-Request
Typische Szenarieninterne Logik, Formulareöffentliche API, Drittanbieter
Codeumfanggeringrelativ höher

Kurz gesagt: intern Server Actions, extern API Routes. Für Formulare in der eigenen App reichen Server Actions. Für Schnittstellen an andere Systeme oder GET-Anfragen brauchen Sie API Routes.

Laut Vercel-Umfrage 2025 setzen bereits 63 % der Entwickler Server Actions in Produktion ein. Das ist kein Experiment mehr.

"63 % der Entwickler nutzen Server Actions bereits in Produktion"

Erstes Server-Actions-Beispiel

Direkt Code – ein einfaches Login-Formular:

// app/login/page.tsx
export default function LoginPage() {
  async function handleLogin(formData: FormData) {
    'use server' // als Server-Funktion markieren

    // Daten aus dem Formular
    const email = formData.get('email') as string
    const password = formData.get('password') as string

    // Login-Logik (vereinfacht)
    console.log('Login-Versuch:', email)

    // In der Praxis: Nutzer prüfen, Token erzeugen usw.
  }

  return (
    <form action={handleLogin}>
      <input
        type="email"
        name="email"
        placeholder="E-Mail"
        required
      />
      <input
        type="password"
        name="password"
        placeholder="Passwort"
        required
      />
      <button type="submit">Anmelden</button>
    </form>
  )
}

So einfach. Die Kernpunkte:

  1. 'use server': Next.js führt die Funktion serverseitig aus
  2. formData.get(): Wert über das name-Attribut des Feldes
  3. action={handleLogin}: beim Submit automatischer Aufruf

Beim Klick auf Absenden lädt die Seite nicht neu – die Daten gehen direkt an den Server. Weniger fetch, useState, Fehlerbehandlung als bei klassischen Ansätzen.

Das ist aber nur die Basis. In echten Projekten brauchen Sie Validierung, Fehleranzeige und Loading. Weiter geht’s.

Formularvalidierung in der Praxis

Validierung mit Zod

Nur required im Client? Zu optimistisch. Mit den Browser-DevTools lassen sich solche Prüfungen leicht umgehen. Serverseitige Validierung ist Pflicht.

Hier kommt Zod ins Spiel: Es prüft Datenformate serverseitig und liefert bei Fehlern sofort strukturierte Meldungen – bevor schlechte Daten in der Datenbank landen.

Zod installieren:

npm install zod

Validierungsregeln definieren:

// app/actions.ts
'use server'

import { z } from 'zod'

// Validierungs-Schema
const SignupSchema = z.object({
  name: z.string().min(2, 'Name mindestens 2 Zeichen'),
  email: z.string().email('Ungültiges E-Mail-Format'),
  password: z.string().min(8, 'Passwort mindestens 8 Zeichen'),
})

export async function signup(formData: FormData) {
  // Daten aus FormData extrahieren
  const rawData = {
    name: formData.get('name'),
    email: formData.get('email'),
    password: formData.get('password'),
  }

  // Validieren
  const result = SignupSchema.safeParse(rawData)

  // Bei Fehler zurückgeben
  if (!result.success) {
    return {
      success: false,
      errors: result.error.flatten().fieldErrors, // Fehler pro Feld
    }
  }

  // Bei Erfolg: Geschäftslogik
  const { name, email, password } = result.data

  // Nutzer anlegen, in DB speichern usw.
  console.log('Nutzer anlegen:', { name, email })

  return {
    success: true,
    message: 'Registrierung erfolgreich!',
  }
}

Wichtige Punkte:

  1. safeParse wirft keine Exception: bei Fehler { success: false, error: ... } – saubere Fehlerbehandlung
  2. flatten().fieldErrors: Fehler als { name: ['Fehler1'], email: ['Fehler2'] } – gut für die Anzeige
  3. Strukturierte Antwort: mit success-Flag und Fehlern – der Client entscheidet, was angezeigt wird

Noch offen: Wie zeigen Sie die Fehler im Formular? Dafür brauchen Sie useActionState.

Validierungsfehler anzeigen: useActionState

useActionState ist ein React-19-Hook (früher useFormState) für den State, den Server Actions zurückgeben. Er:

  • speichert die Server-Antwort im Komponenten-State
  • liefert eine gewrappte action-Funktion
  • zeigt an, ob gerade gesendet wird

Code:

// app/signup/page.tsx
'use client' // Hook erfordert Client-Komponente

import { useActionState } from 'react'
import { signup } from '@/app/actions'

export default function SignupPage() {
  // Initialzustand
  const initialState = { success: false, errors: {}, message: '' }

  // useActionState: Server Action + Initialzustand
  const [state, formAction, isPending] = useActionState(signup, initialState)

  return (
    <form action={formAction}> {/* formAction statt roher action */}
      <div>
        <label>Name</label>
        <input
          type="text"
          name="name"
          required
        />
        {/* Feldfehler anzeigen */}
        {state.errors?.name && (
          <p className="error">{state.errors.name[0]}</p>
        )}
      </div>

      <div>
        <label>E-Mail</label>
        <input
          type="email"
          name="email"
          required
        />
        {state.errors?.email && (
          <p className="error">{state.errors.email[0]}</p>
        )}
      </div>

      <div>
        <label>Passwort</label>
        <input
          type="password"
          name="password"
          required
        />
        {state.errors?.password && (
          <p className="error">{state.errors.password[0]}</p>
        )}
      </div>

      <button type="submit" disabled={isPending}>
        {isPending ? 'Wird gesendet...' : 'Registrieren'}
      </button>

      {/* Erfolgsmeldung */}
      {state.success && (
        <p className="success">{state.message}</p>
      )}
    </form>
  )
}

Ablauf:

  1. Nutzer sendet ab → signup wird aufgerufen
  2. Validierung schlägt fehl → { success: false, errors: {...} }
  3. useActionState speichert das in state
  4. Komponente rendert neu, Fehler werden angezeigt

isPending ist beim Senden true, danach wieder false – für deaktivierten Button und Loading-Text.

Hinweis: Nach Validierungsfehlern gehen eingegebene Werte oft verloren. Sie können beim Zurückgeben ein values-Feld mitgeben und defaultValue setzen – hier der Fokus auf useActionState: Client-Komponente und Server Actions verbinden, State-Management vereinfachen.

Benutzererfahrung optimieren

Loading-Status und Doppel-Submit verhindern

Oben haben wir isPending für Loading genutzt – es gibt noch useFormStatus. Die beiden verwechselt man leicht.

Kurz:

  • isPending von useActionState: in der Formular-Komponente
  • pending von useFormStatus: in Kind-Komponenten (z. B. Submit-Button)

useFormStatus darf nur in <form>-Kindern aufgerufen werden, nicht in der Formular-Komponente selbst. Das klingt umständlich, erlaubt aber wiederverwendbare Button-Komponenten.

Beispiel – Submit-Button auslagern:

// components/SubmitButton.tsx
'use client'

import { useFormStatus } from 'react-dom'

export function SubmitButton({ children }: { children: React.ReactNode }) {
  const { pending } = useFormStatus() // Submit-Status

  return (
    <button
      type="submit"
      disabled={pending}
      className={pending ? 'loading' : ''}
    >
      {pending ? 'Wird gesendet...' : children}
    </button>
  )
}

Im Formular:

// app/signup/page.tsx
'use client'

import { useActionState } from 'react'
import { signup } from '@/app/actions'
import { SubmitButton } from '@/components/SubmitButton'

export default function SignupPage() {
  const [state, formAction] = useActionState(signup, { success: false, errors: {} })

  return (
    <form action={formAction}>
      {/* Formularfelder... */}

      <SubmitButton>Registrieren</SubmitButton> {/* Loading automatisch */}

      {state.errors?.general && (
        <p className="error">{state.errors.general}</p>
      )}
    </form>
  )
}

Die Loading-Logik steckt im Button. Beim Senden:

  • Button wird deaktiviert (kein Doppel-Submit)
  • Text wechselt zu „Wird gesendet…“
  • optional Spinner-Animation

Unterschied pending vs. isPending?

EigenschaftisPending (useActionState)pending (useFormStatus)
AufrufortFormular-KomponenteKind der Formular-Komponente
EinsatzGesamt-State des Formularsunabhängiger Submit-Button
Flexibilitätstate und pending zusammennur pending

In Projekten nutze ich:

  • komplexe Formularlogik, mehrere States → useActionState
  • generischer Submit-Button → useFormStatus

Progressive Enhancement

Server Actions unterstützen Progressive Enhancement: Auch ohne JavaScript funktioniert das Formular.

Technisch nutzen Server Actions das native <form>-Submit. Mit JavaScript fängt Next.js ab und macht daraus eine AJAX-Anfrage; ohne JavaScript bleibt klassisches Formular-Submit.

In der Praxis selten relevant – aber plus für Barrierefreiheit und Crawler. Next.js erledigt das automatisch.

Sicherheit und Best Practices

Sicherheit von Server Actions

Der am leichtesten übersehene Punkt. Viele denken: läuft auf dem Server, also sicher. Falsch.

Server Actions sind im Grunde öffentliche API-Endpunkte. Next.js vergibt eine schwer erratbare ID – das ist Obfuscation, keine echte Sicherheit. Mit DevTools und Netzwerk-Tab findet man die Action-ID und kann sie manuell aufrufen.

Next.js bietet eingebauten Schutz:

  1. CSRF-Schutz: Server Actions nur per POST; Origin- und Host-Header müssen passen – Cross-Site-Requests werden abgelehnt
  2. Sichere Action-ID: verschlüsselte ID pro Action, schwer zu enumerieren
  3. Verschlüsselung von Closure-Variablen: externe Variablen in Actions werden verschlüsselt

Das reicht nicht. Sie müssen zusätzlich:

1. Eingabevalidierung

Client-Daten nie blind vertrauen. Zod-Validierung ist Pflicht.

2. Authentifizierung

Prüfen, ob der Nutzer angemeldet ist. Jede geschützte Action braucht Session-Prüfung.

3. Autorisierung

Angemeldet heißt nicht berechtigt. Nutzer A darf nicht die Daten von Nutzer B löschen – Berechtigung pro Aktion prüfen.

Praxisbeispiel:

// app/actions.ts
'use server'

import { cookies } from 'next/headers'
import { z } from 'zod'

const DeletePostSchema = z.object({
  postId: z.string().min(1),
})

export async function deletePost(formData: FormData) {
  // 1. Eingabe validieren
  const rawData = {
    postId: formData.get('postId'),
  }

  const result = DeletePostSchema.safeParse(rawData)
  if (!result.success) {
    return { success: false, error: 'Ungültige Anfrage' }
  }

  const { postId } = result.data

  // 2. Authentifizierung: angemeldet?
  const cookieStore = await cookies()
  const sessionToken = cookieStore.get('session')?.value

  if (!sessionToken) {
    return { success: false, error: 'Bitte zuerst anmelden' }
  }

  // 3. Aktuellen Nutzer laden
  const currentUser = await getUserFromSession(sessionToken)
  if (!currentUser) {
    return { success: false, error: 'Sitzung abgelaufen' }
  }

  // 4. Autorisierung: gehört der Beitrag dem Nutzer?
  const post = await getPost(postId)
  if (!post) {
    return { success: false, error: 'Beitrag existiert nicht' }
  }

  if (post.authorId !== currentUser.id) {
    return { success: false, error: 'Keine Berechtigung zum Löschen' }
  }

  // 5. Aktion ausführen
  await deletePostFromDB(postId)

  return { success: true, message: 'Erfolgreich gelöscht' }
}

Vollständiger Ablauf: Validierung → Authentifizierung → Autorisierung → Aktion. Alles nötig.

Praktische Bibliothek: next-safe-action – Middleware für einheitliche Validierung, Auth und Fehlerbehandlung:

import { createSafeActionClient } from 'next-safe-action'

// Action-Client mit Auth
const actionClient = createSafeActionClient({
  // Middleware: Login prüfen
  async middleware() {
    const session = await getSession()
    if (!session) {
      throw new Error('Nicht angemeldet')
    }
    return { userId: session.userId }
  },
})

// Automatische Auth-Prüfung
export const deletePost = actionClient
  .schema(DeletePostSchema)
  .action(async ({ parsedInput, ctx }) => {
    const { postId } = parsedInput
    const { userId } = ctx // userId aus Middleware

    // Löschen...
  })

Alle geschützten Actions teilen sich dieselbe Logik – deutlich aufgeräumter Code.

Merken: Server Actions sind kein Zauber – es ist ein API-Endpunkt. Sicherheitsmaßnahmen bleiben Ihre Aufgabe.

Praxis: Formular mit Authentifizierung

Vollständiges Beispiel – Kommentarformular nur für angemeldete Nutzer:

// app/actions.ts
'use server'

import { cookies } from 'next/headers'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'

const CommentSchema = z.object({
  postId: z.string(),
  content: z.string().min(1, 'Kommentar darf nicht leer sein').max(500, 'Maximal 500 Zeichen'),
})

export async function addComment(formData: FormData) {
  // 1. Eingabe validieren
  const rawData = {
    postId: formData.get('postId'),
    content: formData.get('content'),
  }

  const result = CommentSchema.safeParse(rawData)
  if (!result.success) {
    return {
      success: false,
      errors: result.error.flatten().fieldErrors,
    }
  }

  // 2. Authentifizierung
  const cookieStore = await cookies()
  const sessionToken = cookieStore.get('session')?.value

  if (!sessionToken) {
    return {
      success: false,
      error: 'Bitte anmelden, um zu kommentieren',
    }
  }

  const user = await getUserFromSession(sessionToken)
  if (!user) {
    return {
      success: false,
      error: 'Sitzung abgelaufen, bitte erneut anmelden',
    }
  }

  // 3. Kommentar speichern
  const { postId, content } = result.data

  await saveComment({
    postId,
    content,
    authorId: user.id,
    authorName: user.name,
    createdAt: new Date(),
  })

  // 4. Cache invalidieren – Kommentar sofort sichtbar
  revalidatePath(`/posts/${postId}`)

  return {
    success: true,
    message: 'Kommentar veröffentlicht',
  }
}

Client-Komponente:

// app/posts/[id]/CommentForm.tsx
'use client'

import { useActionState } from 'react'
import { addComment } from '@/app/actions'
import { SubmitButton } from '@/components/SubmitButton'

export function CommentForm({ postId }: { postId: string }) {
  const [state, formAction] = useActionState(addComment, {
    success: false,
    errors: {},
  })

  return (
    <form action={formAction}>
      {/* postId als verstecktes Feld */}
      <input type="hidden" name="postId" value={postId} />

      <textarea
        name="content"
        placeholder="Ihr Kommentar..."
        rows={4}
        required
      />

      {state.errors?.content && (
        <p className="error">{state.errors.content[0]}</p>
      )}

      {state.error && (
        <p className="error">{state.error}</p>
      )}

      {state.success && (
        <p className="success">{state.message}</p>
      )}

      <SubmitButton>Kommentar senden</SubmitButton>
    </form>
  )
}

Alles zusammen:

  • Zod-Validierung
  • Login-Prüfung
  • useActionState für State
  • revalidatePath für Cache
  • Submit-Button mit Loading

Produktionsreifer Formular-Workflow.

Fortgeschrittene Techniken

Zusätzliche Parameter übergeben

Manchmal brauchen Sie mehr als Formularfelder – z. B. Beitrags-ID beim Bearbeiten.

Eine Möglichkeit: verstecktes Feld

<input type="hidden" name="postId" value={postId} />

Eleganter: JavaScript-bind

// app/actions.ts
'use server'

export async function updatePost(postId: string, formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  // Beitrag aktualisieren...
  await updatePostInDB(postId, { title, content })

  return { success: true }
}

Client:

// app/posts/[id]/edit/page.tsx
'use client'

import { updatePost } from '@/app/actions'

export default function EditPost({ postId }: { postId: string }) {
  // postId an erste Parameterposition binden
  const updatePostWithId = updatePost.bind(null, postId)

  return (
    <form action={updatePostWithId}>
      <input type="text" name="title" required />
      <textarea name="content" required />
      <button type="submit">Aktualisieren</button>
    </form>
  )
}

bind(null, postId) erzeugt eine neue Funktion mit festem erstem Argument. Beim Submit wird FormData als zweites Argument übergeben.

Typisch für Bearbeiten, Löschen und andere ID-basierte Aktionen.

Cache-Revalidierung

Nach Server Actions kann der Seiten-Cache veraltet sein. Next.js bietet zwei Funktionen:

1. revalidatePath

Nach Pfad invalidieren:

import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  // Beitrag anlegen...

  // Startseiten-Liste aktualisieren
  revalidatePath('/')
  // Detailseite aktualisieren
  revalidatePath(`/posts/${newPostId}`)

  return { success: true }
}

2. revalidateTag

Nach Tag invalidieren (Tags beim fetch setzen):

// Beim Laden taggen
fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] }
})

// In der Server Action alle 'posts'-Caches invalidieren
import { revalidateTag } from 'next/cache'

export async function createPost(formData: FormData) {
  // Beitrag anlegen...

  revalidateTag('posts') // alle zugehörigen Caches

  return { success: true }
}

Wann welches?

  • Feste, wenige PfaderevalidatePath
  • Daten über viele SeitenrevalidateTag

Meist starte ich mit revalidatePath – einfach und direkt. Tags nur, wenn eine Aktion viele Seiten betrifft.

Optimistische Updates

Bei fast immer erfolgreichen Aktionen (Like, Favorit): UI sofort aktualisieren, Submit im Hintergrund.

React 19: useOptimistic

'use client'

import { useOptimistic } from 'react'
import { likePost } from '@/app/actions'

export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(initialLikes)

  async function handleLike() {
    // UI sofort aktualisieren (optimistisch)
    setOptimisticLikes(optimisticLikes + 1)

    // Submit im Hintergrund
    await likePost(postId)
  }

  return (
    <button onClick={handleLike}>
      👍 {optimisticLikes}
    </button>
  )
}

Klick → Zahl steigt sofort, ohne auf den Server zu warten. Sehr flüssig.

Nur bei sehr hoher Erfolgsrate. Bei Fehlern muss die UI zurückgerollt werden – oft mehr Aufwand als Nutzen.

Fazit

Drei Kernpunkte:

  1. Server Actions vereinfachen Formulare, sind aber nicht überall die Lösung. Interne Formulare: ja; externe APIs: Route Handlers. Nicht alles blind auf Server Actions umstellen.

  2. Sicherheit liegt bei Ihnen. Das Framework liefert Grundschutz – Validierung, Auth und Autorisierung müssen Sie selbst implementieren. Next.js ersetzt keine Sicherheitsarchitektur.

  3. UX-Details zählen. Loading, Fehlermeldungen, optimistische Updates – der Unterschied zwischen „geht so“ und „fühlt sich gut an“. Mit useActionState und useFormStatus sauber abbilden.

Starten Sie mit einem einfachen Formular: Server Action, Zod, Loading – damit haben Sie schon 80 % abgedeckt. Cache-Revalidierung und optimistische Updates können warten, bis Sie sie brauchen.

Next.js und React entwickeln sich schnell – Server-Actions-APIs können sich ändern. Offizielle Doku im Blick behalten.

Probieren Sie es im nächsten Projekt aus. Beim nächsten Formular-Submit merken Sie vielleicht: Es kann wirklich so einfach sein.

Vollständiger Workflow: Formulare mit Server Actions verarbeiten

Von der Server Action bis zu Validierung und State-Handling – Schritt für Schritt

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Server Action erstellen

    Server Action in app/actions.ts anlegen:

    1. Datei-Level-Markierung: 'use server' oben in der Datei
    2. Funktion definieren: export async function actionName(formData: FormData)
    3. Daten lesen: formData.get('fieldName') für Formularfelder
    4. Ergebnis zurückgeben: Format { success: boolean, errors?: {}, message?: string }

    Beispiel:
    ```typescript
    'use server'
    export async function signup(formData: FormData) {
    const name = formData.get('name') as string
    // Verarbeitungslogik...
    return { success: true, message: 'Registrierung erfolgreich' }
    }
    ```
  2. 2

    Step 2: Zod-Validierung hinzufügen

    Serverseitige Validierung mit Zod:

    1. Zod installieren: npm install zod
    2. Schema definieren: const SignupSchema = z.object({ name: z.string().min(2), email: z.string().email() })
    3. Daten validieren: const result = SignupSchema.safeParse(rawData)
    4. Fehler behandeln: if (!result.success) return { success: false, errors: result.error.flatten().fieldErrors }

    Wichtig:
    • safeParse wirft keine Exception, liefert { success, data/error }
    • flatten().fieldErrors formatiert Fehler als { field: ['error1'] }
    • Bei Validierungsfehlern strukturierte Fehler zurückgeben, damit der Client sie anzeigen kann
  3. 3

    Step 3: State mit useActionState verarbeiten

    useActionState in Client-Komponenten:

    1. Hook importieren: import { useActionState } from 'react'
    2. Initialzustand: const initialState = { success: false, errors: {} }
    3. Hook nutzen: const [state, formAction, isPending] = useActionState(action, initialState)
    4. Formular binden: <form action={formAction}>
    5. Fehler anzeigen: {state.errors?.field && <p>{state.errors.field[0]}</p>}
    6. Loading anzeigen: <button disabled={isPending}>{isPending ? 'Wird gesendet...' : 'Absenden'}</button>

    Ablauf:
    • Nutzer sendet ab → Action wird aufgerufen → Ergebnis zurück → state aktualisiert → Komponente rendert neu
  4. 4

    Step 4: Authentifizierung und Autorisierung hinzufügen

    Sicherheitsprüfungen in der Server Action:

    1. Eingabevalidierung: alle Eingaben mit Zod prüfen
    2. Authentifizierung: Session-Token prüfen
    ```typescript
    const cookieStore = await cookies()
    const sessionToken = cookieStore.get('session')?.value
    if (!sessionToken) return { success: false, error: 'Bitte zuerst anmelden' }
    ```
    3. Autorisierung: Berechtigung für die Aktion prüfen
    ```typescript
    const post = await getPost(postId)
    if (post.authorId !== currentUser.id) {
    return { success: false, error: 'Keine Berechtigung' }
    }
    ```
    4. Aktion ausführen: nach erfolgreicher Prüfung die Geschäftslogik

    Merken: Server Actions sind keine Magie – Sicherheitsprüfungen sind manuell nötig
  5. 5

    Step 5: Benutzererfahrung optimieren

    Loading-Status und Fehlerbehandlung:

    1. useFormStatus (in Button-Komponente):
    ```typescript
    'use client'
    import { useFormStatus } from 'react-dom'
    export function SubmitButton() {
    const { pending } = useFormStatus()
    return <button disabled={pending}>...</button>
    }
    ```
    2. Cache mit revalidatePath aktualisieren:
    ```typescript
    import { revalidatePath } from 'next/cache'
    revalidatePath('/posts')
    ```
    3. Optimistisches Update (optional, bei sehr hoher Erfolgsrate):
    ```typescript
    const [optimisticState, setOptimisticState] = useOptimistic(initialState)
    ```

    Best Practice:
    • Komplexe Formularlogik → useActionState
    • Unabhängige Button-Komponente → useFormStatus
    • Nach erfolgreicher Aktion → relevanten Seiten-Cache aktualisieren

FAQ

Was ist der Unterschied zwischen Server Actions und API Routes? Wann nutze ich welche?
Server Actions eignen sich für interne Formular-Submits und Datenänderungen – nur POST, typsicher, weniger Code. API Routes eignen sich für RESTful APIs nach außen, GET-Anfragen und Drittanbieter-Integration. Kurz: intern Server Actions, extern API Routes.
Sind Server Actions sicher? Welche Sicherheitsmaßnahmen sind nötig?
Server Actions laufen serverseitig, aber Sicherheitsprüfungen sind manuell nötig: 1) Eingabevalidierung (Zod), 2) Authentifizierung (Session prüfen), 3) Autorisierung (Berechtigungen prüfen). Das Framework bietet nur grundlegenden CSRF-Schutz – darauf allein sollten Sie sich nicht verlassen.
Was ist der Unterschied zwischen useActionState und useFormStatus?
isPending von useActionState eignet sich innerhalb der Formular-Komponente – Sie erhalten gleichzeitig state und pending. pending von useFormStatus muss in einer Kind-Komponente des Formulars (z. B. Button) genutzt werden und liefert nur den pending-Status. Komplexe Formularlogik → useActionState; unabhängige Button-Komponente → useFormStatus.
Wie übergebe ich Parameter außerhalb der Formularfelder?
Zwei Wege: 1) Verstecktes Feld <input type="hidden" name="postId" value={postId} />, 2) bind-Methode: const actionWithId = action.bind(null, postId), dann <form action={actionWithId}>. bind ist eleganter und wird empfohlen.
Wie aktualisiere ich Seitendaten nach dem Formular-Submit?
Mit revalidatePath nach Pfad: revalidatePath('/posts'), oder mit revalidateTag nach Tag (Tags müssen beim fetch gesetzt sein). Feste, wenige Pfade → revalidatePath; Daten über viele Seiten verteilt → revalidateTag.
Wann sollte ich optimistische Updates nutzen?
Optimistische Updates eignen sich für Aktionen mit sehr hoher Erfolgsrate (Likes, Favoriten) – useOptimistic aktualisiert die UI sofort, der Submit läuft im Hintergrund. Bei fehleranfälligen Aktionen eher vermeiden, weil ein Rollback der UI aufwendiger ist.

12 Min. Lesezeit · Veröffentlicht am: 19. Dez. 2025 · Aktualisiert am: 14. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog