Startseite / Artikel / Server-Aktionen oder Route-Handler? Ein Entscheidungshilfe für Next.js 16

Server-Aktionen oder Route-Handler? Ein Entscheidungshilfe für Next.js 16

Erfahren Sie, wann eine Mutation in Next.js 16 in einer Server Action gehören sollte und wann ein Route Handler erforderlich ist, mit korrigierten Beispielen für Formulare, Fehler, Optimismus und Webhooks.

1558 Wörter

Server Actions wurden in Next.js 13.4 als Abkürzung für Formularabsendungen eingeführt und sind heute ein zentrales Element des App Router. Dennoch stellt jede neue Funktion immer wieder dieselbe Frage: Action oder API-Route? Hier finden Sie ein mentales Modell zur Entscheidungsfindung, Beispiele für beide Ansätze im Code sowie Lösungen für Missverständnisse, die zu echten Fehlern führen.

Zwei verschiedene Konzepte

API-Routes, im App Router als Route Handlers bezeichnet (app/api/.../route.ts), sind gewöhnliche HTTP-Endpunkte. Sie verfügen über stabile URLs, die jeder Client aufrufen kann, geben volle Kontrolle über Statuscodes und Caching-Header sowie keinen Zusammenhang mit Ihren Komponenten.

Server Actions sind 'use server'-Funktionen, die aus Ihrem React-Code aufgerufen werden. Next.js erzeugt den Endpunkt, serialisiert Argumente und Ergebnisse und verbindet sie mit Formularen, Übergängen sowie einer optimistischen Benutzeroberfläche, wobei Typinformationen an den Aufrufort zurückgegeben werden.

Gegenüber einer weit verbreiteten Annahme sind Server Actions nicht privat: Jeder von ihnen ist ein POST-Endpunkt, der durch eine Action-ID identifiziert wird, und jeder, der diese ID besitzt, kann ihn mit beliebigen Argumenten aufrufen.

Die Faustregel lautet: Außenstehende Aufrufer erhalten einen Route Handler; Mutationen, die durch Ihre eigene Benutzeroberfläche ausgelöst werden, passen in der Regel zu einer Server Action.

Server Actions in der Praxis

Eine formbasierte Mutation

Die Datei beginnt mit der Anweisung, die jede Exportfunktion als Serverfunktion kennzeichnet:

// app/actions/order.ts
'use server'

Die Action authentifiziert sich, validiert die Felder, speichert die Bestellung ab, validiert erneut /orders und leitet um. auth() liest die Cookies der Anfrage, sodass der Client kein Token übermittelt.

import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'export async function createOrder(formData: FormData) {
  // Auth is automatic — no need to pass session
  const session = await auth()
  if (!session?.user) throw new Error('Unauthorized')  const item = formData.get('item') as string
  const quantity = Number(formData.get('quantity'))  // Validate
  if (!item || quantity < 1) {
    return { error: 'Invalid order data' }
  }  // Database write
  const order = await db.order.create({
    data: { item, quantity, userId: session.user.id },
  })  // Invalidate cached data
  revalidatePath('/orders')  // Redirect after success
  redirect(`/orders/${order.id}`)
}

Beachten Sie, dass redirect durch das Werfen eines Fehlers funktioniert, weshalb es niemals innerhalb eines try-Blocks aufgerufen werden sollte, der Fehler verschluckt. Die Seite importiert die Action:

// app/shop/page.tsx
import { createOrder } from '@/app/actions/order'

und gibt es direkt an die action-Eigenschaft eines Forms weiter:

export default function ShopPage() {
  return (
    <form action={createOrder}>
      <input name="item" type="text" placeholder="Item name" />
      <input name="quantity" type="number" defaultValue={1} />
      <button type="submit">Order</button>
    </form>
  )
}

Kein Client-Zustand, kein Effekt und keine fetch-Anfrage – die Form wird bereits vor dem Laden von JavaScript abgesendet.

Fehler mit useActionState zurückgeben

useActionState (React 19) speichert den letzten Rückgabewert der Aktion sowie ein Flag für ausstehende Operationen:

'use client'
import { useActionState } from 'react'
import { createOrder } from '@/app/actions/order'
export function OrderForm() {
  const [state, action, isPending] = useActionState(createOrder, null)  return (
    <form action={action}>
      {state?.error && (
        <p style={{ color: 'red' }}>{state.error}</p>
      )}
      <input name="item" />
      <input name="quantity" type="number" />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Ordering...' : 'Place Order'}
      </button>
    </form>
  )
}

Eine Einschränkung: Wenn eine Aktion durch useActionState umhüllt wird, erhält sie den vorherigen Zustand als erstes Argument und FormData als zweites. Die obige Signatur von createOrder nimmt nur formData entgegen, sodass sie für diese Form in createOrder(prevState, formData) umgewandelt werden muss.

Optimistische Rückmeldung mit useOptimistic

useOptimistic zeigt das erwartete Ergebnis, bis die Aktion abgeschlossen ist:

'use client'
import { useOptimistic } from 'react'
import { toggleLike } from '@/app/actions/post'
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [optimisticLikes, setOptimistic] = useOptimistic(initialLikes)  async function handleLike() {
    setOptimistic(prev => prev + 1) // UI updates instantly
    await toggleLike(postId)        // Server call happens in background
  }  return (
    <button onClick={handleLike}>
      ❤️ {optimisticLikes}
    </button>
  )
}

Der optimistische Setter muss innerhalb einer Transition oder Aktion ausgeführt werden. Von einem einfachen onClick aus sollte der Codekörper in startTransition eingebettet werden, andernfalls warnt React und der optimistische Zustand verhält sich nicht wie erwartet. Zu den Problemen bei Rollbacks wird in fünf Fehlermodi von useOptimistic erklärt.

Wann ein Route Handler das richtige Werkzeug ist

Dieser versionierte Endpunkt zeigt seine Vorteile: Abfragemethoden, CDN-Caching und Authentifizierung mit API-Schlüsseln.

// app/api/v1/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const category = searchParams.get('category')  const products = await db.product.findMany({
    where: category ? { category } : undefined,
  })  return NextResponse.json({ products }, {
    headers: {
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
    }
  })
}export async function POST(request: NextRequest) {
  const apiKey = request.headers.get('x-api-key')
  if (apiKey !== process.env.API_KEY) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }  const body = await request.json()
  const product = await db.product.create({ data: body })
  return NextResponse.json({ product }, { status: 201 })
}

Wählen Sie einen Route Handler, wenn:

  • Mobilanwendungen den Endpunkt nutzen;
  • eine Drittpartei Webhooks bereitstellt;
  • die Antworten eigene Caching-Header benötigen;
  • es sich um eine öffentliche, versionierte API oder ein API-Produkt handelt.

In der Produktion sollte der POST-Body überprüft werden, anstatt ihn unüberprüft an die Datenbank weiterzuleiten.

Vergleich der beiden Ansätze

  • Wege hin und zurück: Beide erfordern eine Anfrage vom Browser an Ihren Server; keiner der Ansätze erfolgt server-zu-server.
  • Klientenpaket: Die Logik bleibt in jedem Fall auf dem Server.
  • Authentifizierung: Die Aktionen lesen die Session automatisch aus Cookies; die Handler, die anderen Clients dienen, überprüfen Tokens oder Schlüssel pro Anfrage.
  • Typen: Gemeinsam von Ende zu Ende mit den Aktionen; die Handler benötigen einen typisierten Client oder ein Schema.
  • Caching: Die Aktionen machen den Inhalt ungültig mit revalidatePath oder Tags; die Handler setzen Cache-Control.
  • Optimistische Benutzeroberfläche: In Reacts Hooks für Aktionen integriert; bei Handlern manuell umzusetzen.

Zum Zeitpunkt der Erstellung schickt Next.js Server Actions ebenfalls nacheinander vom Client aus, wodurch sie eher für Mutationen als für parallele Dateneinsellungen geeignet sind; prüfen Sie die aktuellen Dokumentationen.

Entscheidungskontrolle

Fangen Sie mit der Frage an, die in den meisten Fällen entscheidend ist:

Does an external system call this?
  YES → API Route

Dann gehen Sie mit den restlichen Punkten weiter:

Is this triggered by a form submit or user action within your UI?
  YES → Server ActionDo you need explicit HTTP caching headers?
  YES → API RouteDo you want automatic auth context without passing tokens?
  YES → Server ActionDo you need to call this from a mobile app?
  YES → API RouteEverything else?
  → Server Action (less boilerplate)

Zwei Fehler, die vermieden werden sollten

Webhooks auf eine Server Action richten

Stripe sendet Daten an eine registrierte URL und kann nicht auf einen Action-ID zugegriffen werden.

// ❌ Wrong — Server Actions can't receive arbitrary HTTP POSTs from Stripe
'use server'
export async function handleStripeWebhook() { ... }

Webhooks gehören in einen Route Handler, wo Sie den Rohkörper lesen und die Signatur überprüfen können.

// ✅ Correct
// app/api/webhooks/stripe/route.ts
export async function POST(request: NextRequest) { ... }

Vertrauen in Eingaben der Action

Weil jede Action direkt aufgerufen werden kann, ermöglicht eine löschen-Action ohne Überprüfungen jedem, alles zu löschen:

// ❌ Wrong — Server Actions are not inherently trusted
'use server'
export async function deletePost(postId: string) {
  await db.post.delete({ where: { id: postId } }) // Anyone can call this!
}

Die Lösung überprüft vor dem Schreiben die Sitzung und das Eigentumsrecht und validiert anschließend erneut:

// ✅ Correct — always validate auth + ownership
'use server'
export async function deletePost(postId: string) {
  const session = await auth()
  const post = await db.post.findUnique({ where: { id: postId } })
  if (post?.userId !== session?.user?.id) throw new Error('Forbidden')
  await db.post.delete({ where: { id: postId } })
  revalidatePath('/posts')
}

Weisen Sie fehlende Sessions daher explizit ab. Unser Artikel zu Authorization innerhalb jeder Server Action behandelt dieses Thema ausführlich.

Hauptpunkte

  • Jede Server Action ist ein erreichbarer Endpunkt; authentifizieren, autorisieren und validieren Sie dort.
  • Mit useActionState erhält die Action zunächst den vorherigen Zustand.
  • Führen Sie useOptimistic-Aktualisierungen innerhalb einer Übertragung aus.

Zusätzliche Literatur

  • Warum Next.js Server Actions eine Autorisierung innerhalb des Funktionskörpers benötigen — Ein detaillierter Fall von Account-Übernahme zeigt, wie unauthentifizierte Next.js Server Actions privilegierte Operationen freigeben, und wo die Autorisierungsprüfung stattfinden muss, um dies zu verhindern.
  • React 19 Upgrade-Triage: Welche neuen APIs ersetzen Ihre Workarounds — Ein praktischer Überblick über use, Server Actions, useOptimistic und den React Compiler, inklusive wichtiger Einschränkungen sowie einem Plan dafür, welche Änderungen in React 19 zuerst umgesetzt werden sollten.
  • Wo man die Server-Client-Grenze in einer Next.js App Router Seite zieht — Ein praktisches mentales Modell für React Server Components: Was wo ausgeführt wird, wie eine Blogartikelseite in Server- und Client-Bereiche aufgeteilt wird sowie die einzuhaltenden Importregeln.
  • Next.js oder reines React? Die Auswahl des Frameworks für Ihr Projekt — Eine auf Anforderungen ausgerichtete Methode, um zu entscheiden, ob ein neues React-Projekt Next.js benötigt, mit Schwerpunkt auf SEO, hybrider Darstellung, Hosting-Beschränkungen, Routing-Anforderungen und Teamerfahrung.
  • Eine einzige Quelle der Wahrheit für Route-Pfade in Next.js-Anwendungen — Erfahren Sie, wie ein typisierter zentraler Routes-Modul die verstreuten String-Pfade in Next.js ersetzt, zusammen mit Hilfsfunktionen für dynamische Segmente und gruppierte Route-Exporte.
  • Warum WebSockets in Next.js API-Routen hängen bleiben und wie ein benutzerdefinierter Server das behebt — Erfahren Sie, warum ein ws-Server innerhalb einer pages/api-Route niemals die Verbindungsherstellung abschließt, und wie Sie das Upgrade-Event mit einem benutzerdefinierten Next.js-Server selbst handhaben können.