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.
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
revalidatePathoder Tags; die Handler setzenCache-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
useActionStateerhält die Action zunächst den vorherigen Zustand. - Führen Sie
useOptimistic-Aktualisierungen innerhalb einer Übertragung aus.
Zusätzliche Literatur
- useOptimistic Rollback: Fünf Fehlermodi in Next.js Server Actions – Erfahren Sie, warum useOptimistic die Benutzeroberfläche stumm zurücksetzt, ohne den Nutzern Fehler zu erklären, anhand von fünf getesteten Fehlermodi von Server Actions sowie einer funktionierenden Lösung.