Next.js Server Actions vs API Routes vs tRPC: Das richtige Data-Mutation-Pattern wählen

#Server Actions vs API Routes
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Jede Next.js-15-Codebasis beherbergt irgendwann dieselbe Diskussion. Ein Formular muss in die Datenbank schreiben, und es gibt drei vernünftige Wege, das zu verdrahten: eine Server Action, einen Route Handler oder eine tRPC-Procedure. Die Frage Server Actions vs API Routes vs tRPC taucht in fast jedem Next.js-Architektur-Review auf, das wir durchführen, meist weil ein Team in Woche eins ein Pattern gewählt hat und in Monat sechs anfing, für diese Wahl zu bezahlen.

Alle drei sind legitime Werkzeuge. Sie scheitern aber an unterschiedlichen Stellen, und diese Fehler zeigen sich selten, bevor Produktions-Traffic da ist. Dieser Beitrag geht durch, was jedes Pattern gut kann, wie der Code in Next.js 15 mit React 19 aussieht, welche Security-Checks jedes braucht und welchen Default wir für B2B-SaaS-Projekte empfehlen.

Server Actions vs API Routes: der Unterschied, der zählt

Zieh die Syntax ab, und die beiden Patterns unterscheiden sich in einem wichtigen Punkt: Wer besitzt den Contract.

Eine Server Action hat keinen öffentlichen Contract. Der Compiler generiert einen RPC-Endpoint für dich, die Funktionssignatur lebt neben der Komponente, die sie aufruft, und der einzige Consumer ist dein eigener React-Tree. Benenne ein Argument um, und TypeScript aktualisiert jede Aufrufstelle. Es gibt keine URL zu dokumentieren, weil du nie eine siehst.

Ein Route Handler ist das Gegenteil. Du definierst eine URL, eine Methode, eine Request-Form und eine Response-Form, und alles, was HTTP spricht, kann ihn aufrufen. Der Contract ist explizit, was bedeutet, dass du ihn pflegen, versionieren und alles Ankommende validieren musst.

tRPC sitzt dazwischen. Procedures sind unter der Haube echte HTTP-Endpoints, aber der Client wird aus dem Router-Typ generiert. Du bekommst also die explizite Struktur einer API mit der Aufruf-Ergonomie einer Funktion.

Server ActionsRoute HandlerstRPC
AufruferNur deine React-AppJeder HTTP-ClientTypeScript-Clients mit geteiltem Router-Typ
ContractImplizit, vom Compiler verwaltetExplizite URL + SchemaTypisierte Procedures
Streaming-ResponsesNeinJaBegrenzt (Subscriptions brauchen extra Setup)
Error-TypisierungGrobWas immer du designstTypisierte Error-Shapes
Isoliertes TestingUmständlichEinfache HTTP-TestsUnkompliziert mit einem Test-Caller
Backend-VoraussetzungNext.js-ServerNext.js-ServerNode.js-Runtime

Wo Server Actions ihr Geld wert sind

Für Formular-Submissions und einfache Mutationen mit einem Zweck sind Server Actions beim Aufwand kaum zu schlagen. Ein Create-Project-Flow braucht keine Endpoint-Datei, keinen Fetch-Wrapper und keine Client-State-Bibliothek:

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

import { revalidatePath } from 'next/cache'
import { requireUser } from '@/lib/auth'
import { projectSchema } from '@/lib/schemas'

export async function createProject(formData: FormData) {
  const user = await requireUser()
  const parsed = projectSchema.parse(Object.fromEntries(formData))
  await db.project.create({ data: { ...parsed, ownerId: user.id } })
  revalidatePath('/projects')
}

Kombiniere das mit useActionState in React 19 und du bekommst Pending-States und Progressive Enhancement ohne zusätzliche Verdrahtung. Formulare funktionieren schon vor der Hydration, was auf langsamen Verbindungen immer noch zählt.

Die Grenzen sind genauso konkret. Actions können keine Response streamen, alles mit inkrementellem Output, zum Beispiel eine LLM-Completion, braucht stattdessen einen Route Handler. Rückgabewerte müssen die Serialisierung überleben. In Produktion geworfene Errors überqueren die Grenze als generische Meldung, feingranulares Error-Handling heißt also Discriminated Unions zurückgeben statt werfen. Actions eines einzelnen Clients laufen sequenziell, was Teams überrascht, die mehrere parallel abfeuern. Und weil eine Action das Framework um sich herum braucht, ist isoliertes Unit-Testing unhandlicher als bei einer einfachen Funktion.

Die Produktionsseite davon haben wir ausführlicher in Server Actions in production behandelt, inklusive doppelter Submissions und Optimistic UI.

Wann ein Route Handler die richtige Wahl ist

In dem Moment, in dem eine Mutation einen Consumer hat, der nicht dein eigener React-Tree ist, willst du einen echten Endpoint. Mobile Apps, Partner-Integrationen, eingehende Webhooks von Stripe oder GitHub und alles, wogegen ein Kunde skripten könnte, brauchen eine URL mit dokumentierter Form:

// app/api/v1/projects/route.ts
import { NextResponse } from 'next/server'
import { authenticateRequest } from '@/lib/auth'
import { projectSchema } from '@/lib/schemas'

export async function POST(request: Request) {
  const user = await authenticateRequest(request)
  if (!user) {
    return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
  }
  const body = projectSchema.safeParse(await request.json())
  if (!body.success) {
    return NextResponse.json({ error: body.error.flatten() }, { status: 422 })
  }
  const project = await db.project.create({ data: { ...body.data, ownerId: user.id } })
  return NextResponse.json(project, { status: 201 })
}

Du schreibst mehr Code und bekommst dafür Dinge, die Server Actions dir nicht geben können: Streaming-Responses, richtige Status-Codes, Response-Caching, einen versionierbaren Pfad (/api/v1/) und eine Oberfläche, die du mit nichts als einem HTTP-Client testen kannst. Rate Limiting und Idempotency-Keys passen hier ebenfalls natürlich rein, da du den rohen Request kontrollierst.

Der Preis ist Drift. Nichts zwingt Client und Handler, sich über Shapes einig zu sein, Schema-Validierung auf beiden Seiten hört also auf, optional zu sein.

Was tRPC hinzufügt und was es kostet

tRPC schließt genau diese Drift. Der Router ist die Single Source of Truth, der Client wird aus seinem Typ abgeleitet, und ein Breaking Change auf dem Server wird zu einem Compile-Fehler in der Komponente, die ihn aufruft:

// server/routers/project.ts
export const projectRouter = router({
  create: protectedProcedure
    .input(projectSchema)
    .mutation(({ ctx, input }) => {
      return ctx.db.project.create({
        data: { ...input, ownerId: ctx.user.id },
      })
    }),
})

Obendrauf bekommst du React-Query-Integration, Caching, Retries und Invalidierung kommen also aus einer ausgereiften Bibliothek statt aus handgerollten Hooks.

Die Kosten sind real. tRPC setzt ein Node.js-Backend (oder ein Edge-kompatibles) voraus, das Typen mit dem Frontend teilt. Lebt deine API in Symfony, wie bei vielen unserer Kunden, ist tRPC für diese Grenze schlicht vom Tisch; du würdest stattdessen TypeScript-Typen aus einer OpenAPI-Spezifikation generieren. Es fügt außerdem eine Schicht hinzu, die jeder neue Entwickler lernen muss, und Procedures sind ohne zusätzliche REST-Fassade nicht von Dritten konsumierbar. Für eine kleine App mit einem Dutzend Mutationen zahlt sich das Setup selten aus. Für eine große reine TypeScript-Codebasis mit mehreren Entwicklern, die wöchentlich die API anfassen, meistens schon.

Der Security-Check für jedes Pattern

Server Actions sehen aus wie lokale Funktionsaufrufe, und genau das ist die Falle. Jede exportierte Action ist ein öffentlicher HTTP-Endpoint. Das Framework prüft den Request-Origin gegen den Host, um Cross-Site-POSTs zu blocken, aber dieser Check leistet nichts für die Autorisierung. Die erste Zeile jedes Action-Bodys sollte also feststellen, wer aufruft und ob die Person das darf. Behandle jedes Argument als nicht vertrauenswürdigen Input, denn Angreifer sind nicht auf die Werte beschränkt, die dein Formular rendert. Läufst du hinter einem Proxy, prüfe, ob die Forwarded-Host-Header korrekt gesetzt sind, sonst kommt der Origin-Check selbst durcheinander.

Route Handlers mit Cookie-basierten Sessions brauchen CSRF-Schutz, den du selbst baust, denn Next.js fügt für Handler keinen hinzu. Token-basierte Auth umgeht das, wirft aber die üblichen Fragen zu Speicherung und Rotation auf. So oder so: Validiere den Payload mit einem Schema, bevor er die Datenbank berührt.

Bei tRPC konzentriere Authentifizierung in der Context-Factory und Autorisierung in Middleware wie protectedProcedure, statt Checks in einzelne Resolver zu streuen. Zentralisierte Middleware ist das wichtigste Security-Argument für das Pattern: Eine ungeschützte Procedure fällt im Review auf.

Unser Default für B2B-SaaS-Projekte

Für ein typisches B2B-SaaS auf Next.js 15 empfehlen wir eine langweilige Aufteilung. Nutze Server Actions für interne, formularförmige Mutationen: Settings, CRUD-Screens, Onboarding-Schritte. Nutze Route Handlers für jede Grenze, die Außenstehende berühren, also Webhooks, öffentliche APIs, File-Uploads und Streaming. Ergänze tRPC nur, wenn das Team komplett auf TypeScript arbeitet, das Backend Node ist und die API-Oberfläche groß genug, dass Type-Drift zwischen Client und Server dich schon gebissen hat.

Patterns innerhalb einer App zu mischen ist in Ordnung und normal. Der Fehler, den wir in Code-Reviews markieren, ist dieselbe Mutation gleichzeitig über zwei Patterns zu exponieren, denn auf dem zweiten Pfad fehlt dann der Autorisierungs-Check.

Pattern-Entscheidungen wie diese verstärken sich über die Lebensdauer eines Projekts, weshalb sie früh eine Stunde bewusstes Nachdenken wert sind. Es ist die Art von Entscheidung, die wir mit Kunden in Tech-Stack-Strategie-Projekten durchgehen, und in Custom-Software-Development-Projekten wenden wir die obige Aufteilung an, sofern nichts Spezifisches dagegen spricht.

Wenn du auf eine Next.js-Codebasis starrst, in der Mutationen organisch über alle drei Patterns gewachsen sind und niemand mehr weiß warum, helfen wir dir beim Entwirren. Schreib an hello@wolf-tech.io oder besuche wolf-tech.io.

FAQ

Kann ich Server Actions und Route Handlers in derselben Next.js-App nutzen?

Ja, und die meisten Produktions-Apps sollten das. Behalte Server Actions für Mutationen, die nur deine eigenen Komponenten auslösen, und gib allem Externen einen Route Handler. Vermeide nur, dieselbe Operation über beide zu exponieren.

Ersetzen Server Actions API Routes in Next.js 15?

Nein. Server Actions decken den internen Formular-und-Mutation-Fall gut ab, aber Route Handlers bleiben die einzige Option für Streaming, Webhooks, versionierte öffentliche APIs und Nicht-React-Clients.

Funktioniert tRPC mit einem Symfony- oder PHP-Backend?

Nicht über diese Grenze hinweg. tRPC braucht geteilte TypeScript-Typen zwischen Client und Server. Mit einer Symfony-API generierst du Typen aus einer OpenAPI-Spezifikation und rufst die API stattdessen aus Route Handlers oder Server Components auf.