Server Actions in der Produktion: Was niemand über Next.js Mutations sagt

#Server Actions Produktion
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Server Actions kamen in Next.js 14 als Möglichkeit, die Lücke zwischen Formular-Submission und serverseitiger Datenmutation zu schließen. Das Versprechen ist verlockend: keine API-Route zu schreiben, kein fetch-Aufruf zu verdrahten, einfach eine mit 'use server' markierte Funktion direkt aus einer Komponente aufrufen. In Demos und Tutorials fühlt sich das fast magisch an.

In der Produktion fühlt es sich anders an.

Nach dem Ausliefern mehrerer React/Next.js-Anwendungen, in denen Server Actions über Spielzeug-Formulare hinausgingen und in echte Mutations-Flows einflossen - Bestellverarbeitung, SaaS-Onboarding, mehrstufige Assistenten - kristallisiert sich eine konsistente Menge von Fehlermodi heraus. Keiner davon ist ein Blocker. Alle sind behebbar. Aber man muss wissen, dass sie existieren, bevor man sie morgens um 2 Uhr an einem Montag trifft.

Dieser Beitrag behandelt, was diese Fehlermodi sind, die Muster, die sie adressieren, und die ehrliche Antwort darauf, wann man Server Actions ganz überspringen und stattdessen eine einfache API-Route verwenden sollte.


Warum Server Actions in Demos besser aussehen als sie sind

Eine Demo-Server-Action sieht typischerweise so aus:

// app/actions.ts
'use server'

export async function createProject(formData: FormData) {
  const name = formData.get('name') as string
  await db.project.create({ data: { name } })
  revalidatePath('/projects')
}

An ein Formular binden und fertig. Das Framework übernimmt den POST, React rendert mit frischen Daten neu, und man hat die Netzwerkschicht nie direkt berührt.

Was die Demo nicht zeigt: Was passiert, wenn db.project.create einen Fehler wirft, was passiert, wenn der Nutzer zweimal auf Absenden klickt, was passiert, wenn ein Netzwerkausfall dazu führt, dass die Action ausgeführt wird, aber die Antwort nie ankommt - und kritisch: was passiert, wenn die Funktion ohne ordentliche Autorisierungsprüfungen aufgerufen wird.

Das sind keine Randfälle. Das ist der Normalzustand des Produktions-Traffics.


Fehlermodus 1: Fehler, die lautlos verschwinden

Server Actions werfen Fehler auf dieselbe Weise wie jeder andere serverseitige Code. Das Problem ist, wohin diese Fehler gehen.

Wenn eine unbehandelte Ausnahme aus der Action entkommt, stellt Next.js sie als Error Boundary dar. In der Entwicklung bekommt man ein vollständiges Stack-Trace-Overlay. In der Produktion bekommt man, was auch immer das nächste error.tsx rendert - normalerweise eine generische "Etwas ist schiefgelaufen"-Seite, die dem Nutzer keine verwertbaren Informationen gibt und kein Telemetrie liefert.

Schlimmer noch: Wenn die Action in einem Formular ist, das keinen Pending-State verwendet (dazu gleich mehr), bemerkt der Nutzer möglicherweise nicht einmal, dass die Mutation fehlgeschlagen ist. Das Formular scheint abzuschicken. Nichts passiert. Er versucht es erneut.

Die Lösung: Strukturierte Ergebnisse zurückgeben statt werfen.

'use server'

type ActionResult =
  | { success: true; projectId: string }
  | { success: false; error: string }

export async function createProject(formData: FormData): Promise<ActionResult> {
  try {
    const name = (formData.get('name') as string)?.trim()
    if (!name) return { success: false, error: 'Projektname ist erforderlich.' }

    const project = await db.project.create({ data: { name } })
    revalidatePath('/projects')
    return { success: true, projectId: project.id }
  } catch (err) {
    // Hier in die Observability-Plattform loggen
    console.error('[createProject]', err)
    return { success: false, error: 'Projekt konnte nicht erstellt werden. Bitte erneut versuchen.' }
  }
}

Die Client-Komponente prüft dann result.success und rendert entsprechend Feedback. Das hält die Fehlerbehandlung explizit und bietet einen Platz zum Einbinden von Sentry, Datadog oder welchem Observability-Stack auch immer man betreibt.


Fehlermodus 2: Doppelte Submissions unter Last

Das ist derjenige, der am härtesten trifft. Ein Nutzer mit einer langsamen Verbindung sendet ein Formular ab. Die Anfrage erreicht den Server und die Mutation wird ausgeführt. Aber die Antwort verzögert sich - ein kalter Serverless-Function-Start, eine langsame Datenbankabfrage, eine kurze Netzwerkspitze. Der Nutzer sieht kein Feedback. Er klickt erneut.

Jetzt sind zwei identische Mutations unterwegs. Wenn die Server Action nicht idempotent ist, entstehen zwei Datensätze, zwei E-Mails, zwei Abbuchungen, zwei Was-auch-immer-einmal-passieren-sollte.

Die Lösung hat zwei Teile.

Erstens, den Absende-Button während der Action-Ausführung mit useActionState (früher useFormState) und useFormStatus deaktivieren:

'use client'

import { useActionState } from 'react'
import { createProject } from './actions'

export function CreateProjectForm() {
  const [result, action, isPending] = useActionState(createProject, null)

  return (
    <form action={action}>
      <input name="name" required />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Erstelle...' : 'Projekt erstellen'}
      </button>
      {result && !result.success && (
        <p className="text-red-600">{result.error}</p>
      )}
    </form>
  )
}

Zweitens, einen Idempotenz-Key für Mutations hinzufügen, die absolut nicht dupliziert werden dürfen - Zahlungen, E-Mails, Provisionierung. Eine UUID auf dem Client vor der Submission generieren, als verstecktes Feld übergeben und serverseitig prüfen, bevor die Ausführung erfolgt.

'use server'

export async function createProject(formData: FormData) {
  const idempotencyKey = formData.get('idempotencyKey') as string
  const existing = await db.project.findUnique({ where: { idempotencyKey } })
  if (existing) return { success: true, projectId: existing.id } // Replay-sicher
  // ... Weiter mit der Erstellung
}

Fehlermodus 3: Optimistisches UI, das sich falsch anfühlt

Optimistisches UI - die Oberfläche sofort aktualisieren, bevor der Server bestätigt - ist das richtige Muster für jede Mutation, die Nutzer häufig ausführen: einen Tag hinzufügen, eine Checkbox umschalten, Elemente neu anordnen. Server Actions unterstützen das über useOptimistic.

Das Unhandliche ist die Fehlerwiederherstellung. Wenn das optimistische Update visuell erfolgreich ist, aber die Server Action fehlschlägt, muss ein Rollback erfolgen - und das Rollback muss sich natürlich anfühlen, nicht wie eine störende Korrektur.

'use client'

import { useOptimistic, useActionState } from 'react'
import { toggleTodo } from './actions'

export function TodoItem({ todo }: { todo: Todo }) {
  const [optimisticTodo, setOptimistic] = useOptimistic(
    todo,
    (state, newCompleted: boolean) => ({ ...state, completed: newCompleted })
  )

  async function handleToggle() {
    setOptimistic(!optimisticTodo.completed)
    const result = await toggleTodo(todo.id, !todo.completed)
    if (!result.success) {
      // React setzt den optimistischen State beim nächsten Re-Render automatisch zurück,
      // aber explizites Feedback hilft Nutzern zu verstehen, was passiert ist
    }
  }

  return (
    <li onClick={handleToggle} className={optimisticTodo.completed ? 'line-through' : ''}>
      {todo.title}
    </li>
  )
}

Wenn toggleTodo ein Fehlerergebnis zurückgibt, setzt Reacts Reconciliation den optimistischen State beim nächsten Render zurück. Das Element "deaktiviert sich selbst". Das kann verwirrend wirken, wenn es stillschweigend geschieht - kombiniere es mit einer Toast-Benachrichtigung, damit Nutzer verstehen, warum die Action fehlgeschlagen ist.


Fehlermodus 4: Autorisierungslücken

Das ist die gefährlichste, und die Demos adressieren sie nie.

Server Actions sind HTTP-Endpunkte. Es sind POST-Anfragen an die Anwendung. Jeder, der eine POST-Anfrage erstellen kann - einschließlich anderer authentifizierter Nutzer der Anwendung - kann sie aufrufen. Die Tatsache, dass die Action nur über eine bestimmte Komponente in der UI zugänglich gemacht wird, bedeutet nicht, dass jemand sie nicht direkt aufrufen kann.

// Diese Action ist nicht geschützt, unabhängig davon, wie die UI Nutzer weiterleitet
'use server'

export async function deleteProject(projectId: string) {
  await db.project.delete({ where: { id: projectId } })
  revalidatePath('/projects')
}
// Immer Autorisierung innerhalb der Action prüfen
'use server'

import { auth } from '@/lib/auth'

export async function deleteProject(projectId: string) {
  const session = await auth()
  if (!session?.user) throw new Error('Nicht authentifiziert')

  const project = await db.project.findUnique({
    where: { id: projectId },
    select: { ownerId: true },
  })

  if (!project || project.ownerId !== session.user.id) {
    throw new Error('Nicht autorisiert')
  }

  await db.project.delete({ where: { id: projectId } })
  revalidatePath('/projects')
}

Jede Server Action wie einen öffentlichen API-Endpunkt behandeln. Eingaben validieren. Authentifizierung prüfen. Autorisierung auf Ressourcenebene prüfen, nicht nur auf Routen-Ebene. Wenn eine Bibliothek wie zod für Eingabe-Validierung auf API-Routen verwendet wird, auch hier nutzen.

Das ist einer der Orte, wo die "nur eine Funktion"-Abstraktion zusammenbricht. Die Funktion ist nicht nur eine Funktion - sie ist ein RPC-Endpunkt ohne standardmäßige Vertragsdurchsetzung.


Wann Server Actions überspringen und eine API-Route verwenden

Server Actions sind gut geeignet für: Formular-Submissions, Einzelressourcen-Mutations, Mutations, die eng mit den Daten einer einzelnen Seite verbunden sind, und interne Tools, bei denen Progressive Enhancement eine Rolle spielt.

Sie sind schlecht geeignet für:

Webhooks und externe Verbraucher. Ein Drittanbieter-Dienst kann eine Server Action nicht aufrufen. Wenn eine Mutation extern ausgelöst werden muss, ist ein route.ts-API-Handler erforderlich.

Komplexe Mutations-Pipelines. Wenn eine Mutation Koordination zwischen mehreren asynchronen Schritten, Kompensationstransaktionen oder einen Event-Bus erfordert, wird das "nur eine Funktion"-Modell unhandlich. Eine Service-Schicht, die sowohl aus einer API-Route als auch aus einer Action aufgerufen wird, ist sauberer.

Hochdurchsatz-Hintergrundverarbeitung. Server Actions werden synchron innerhalb der Anfrage ausgeführt. Wenn eine Mutation einen lang laufenden Job auslöst, in einer API-Route in die Warteschlange stellen, wo man mehr Kontrolle über Timeouts und Retry-Verhalten hat.

Multi-Tenant-SaaS mit komplexen Berechtigungsmodellen. Wenn Autorisierungslogik nicht trivial ist, ist deren Zentralisierung in Middleware und API-Layer-Guards einfacher zu prüfen als sie über Dutzende von einzelnen Action-Dateien zu verteilen.

Bei Wolf-Tech ist unser Ansatz in Kundenprojekten, standardmäßig Server Actions für unkomplizierte CRUD-Mutations auf Next.js-gerenderten Seiten zu verwenden und auf API-Routen umzuschwenken, sobald die Mutations-Logik geteilt, isoliert getestet oder von etwas anderem als der Seite verbraucht werden muss, die sie initiiert. Die Grenze ist normalerweise klar, sobald man fragt: "Muss irgendetwas anderes als diese Komponente diese Mutation auslösen?"


Eine Produktions-Checkliste für Server Actions

Vor dem Ausliefern einer Server Action in die Produktion prüfen:

  • Alle Fehler werden abgefangen und als strukturierte Ergebnisse zurückgegeben - keine unbehandelten Throws, die die Error Boundary erreichen
  • Submit-Steuerelemente sind während des Pending-States deaktiviert
  • Idempotenz-Keys sind für jede Mutation vorhanden, die nicht dupliziert werden darf (Zahlungen, E-Mails, Provisionierung)
  • Authentifizierung wird innerhalb der Action geprüft, nicht aus der UI-Route angenommen
  • Autorisierung wird auf Ressourcenebene geprüft (nicht nur "ist dieser Nutzer eingeloggt")
  • Eingaben werden mit einer Schema-Bibliothek validiert, bevor Datenbankaufrufe erfolgen
  • Observability-Hooks (Fehler-Logging, Tracing) sind vorhanden

Keine dieser Anforderungen braucht zusätzliche Bibliotheken. Es sind Muster, keine Pakete. Sie konsequent anzuwenden ist das, was eine Server Action, die in einer Demo funktioniert, von einer, die unter echtem Traffic mit echten Nutzern funktioniert, unterscheidet.


Wenn du eine Next.js-Anwendung baust und ein zweites Augenpaar auf deine Mutations-Schicht möchtest - ob Server Actions, API-Routen oder eine Mischung - bietet Wolf-Tech Architektur-Reviews und Code-Qualitäts-Consulting für Teams, die schneller vorankommen wollen, ohne Zuverlässigkeitsrisiken einzuführen. Erreichbar unter hello@wolf-tech.io oder auf wolf-tech.io.