Start / Cheat Sheets

Cheat Sheet

Next.js 16 Full-Stack — Cheat Sheet

Stand: · Next.js 16 Full-Stack-Entwicklung mit React und TypeScript

Next.jsServer ActionsAutorisierungDeployment

Der Spickzettel für alles, was schreibt, schützt und in Betrieb geht: Formulare und Server Actions, eigene Endpunkte, Datenbank, Anmeldung, Rechte, Tests und Deployment. Was davor kommt — Routing, Layouts, die Server-Client-Grenze, Streaming und Caching — steht auf dem ersten Blatt: Next.js 16 Kern.

Maßgeblich ist und bleibt die offizielle Next.js-Dokumentation; dieses Blatt ist eine Auswahl für den Alltag, keine Referenz. Es beschreibt Next.js 16.3 mit React 19.2.

Formulare und Server Actions

'use client'
const [stand, tun, warte] = useActionState(buchen, null)
return <form action={tun}>…<button disabled={warte}>Buchen</button></form>

Der Wartezustand kommt aus dem Hook, nicht aus eigenem useState, und gehört an die Schaltfläche statt an die Seite. Mit useActionState ändert sich die Signatur der Aktion: Der Zustand kommt als erstes Argument.

'use server'
export async function buchen(_stand, formData: FormData) {
  // 1 Berechtigung  2 Validierung  3 Änderung  4 Tag invalidieren  5 schmal zurückgeben
}

Ein Formular in einer Server Component sendet klassisch per POST — die Aktion läuft, auch wenn das Bundle noch nicht geladen ist. Fortschrittliche Verbesserung ist damit der Standardfall, nicht die Kür.

Ziel nach der AktionMittel
auf derselben Seite bleibenZustand zurückgeben
zu einer anderen Seiteredirect in der Aktion
Liste aktualisierenupdateTag oder revalidatePath
Router auffrischenrefresh

redirect wirft — erst invalidieren, dann umleiten.

Modul: Formulare und Server Actions

Validierung und Rückgabe

const geprueft = BuchungSchema.safeParse({ liegeplatz: formData.get('liegeplatz') })
if (!geprueft.success) return geprueft.error.flatten()   // safeParse wirft nicht

Die Aktion ist per direktem POST erreichbar, ohne dass ein Formular beteiligt ist; required im HTML ist in Sekunden entfernt. Verbindlich ist allein die Prüfung, die nicht umgangen werden kann. FormData liefert Zeichenketten, ein fehlendes Feld kommt als null — und fachliche Regeln wie Überschneidungsfreiheit prüft kein Schema.

await db.buchung.create({ data })
return { erfolg: true, nummer: buchung.nummer }   // nicht den ganzen Datensatz

Optimistische Oberflächen lohnen nicht überall — die Frage lautet nicht, ob es geht, sondern was ein Rückzieher kostet:

VorgangOptimistischGrund
Merken, Notiz ändernjafolgenlos, leicht korrigierbar
Buchung bestätigenneinverbindlich
Rechnung stornierenneinGeldfluss

Bei Uploads ist der gemeldete Inhaltstyp eine Behauptung des Clients: Größe und Inhalt zusätzlich prüfen, und die Datei in einen Objektspeicher legen — das Dateisystem des Servers ist beim nächsten Deploy leer.

Modul: Formulare und Server Actions

Aktion oder Route Handler

VerbraucherAktionHandler
eigenes Formularja
eigene Client-Komponentejaja
fremdes System, Webhookja
mobile Appja
Export als Dateija

Im Zweifel Aktion — ein Endpunkt ist eine öffentliche Zusage, die versioniert und gepflegt werden will. Route Handlers aus Server Components aufzurufen ist ein Umweg über das Netz, von dem die Doku ausdrücklich abrät.

export async function GET(request: NextRequest) {
  const sitzung = await verifySession()
  if (!sitzung) return new Response(null, { status: 401 })
  if (sitzung.rolle !== 'verwaltung') return new Response(null, { status: 403 })
  return NextResponse.json(await getRechnungen())
}

Der Endpunkt hat eine eigene URL: Niemand muss dafür die Oberfläche geöffnet haben. Nur exportierte Methoden existieren, alles andere antwortet 405; eine route.ts und eine page.tsx im selben Ordner schließen sich aus.

Modul: Route Handlers und Proxy

Proxy statt Middleware

// proxy.ts — eine Datei je Projekt, neben app/, nicht darin
export function proxy(request: NextRequest) { /* … */ }
export const config = { matcher: '/dashboard/:path*' }

Der Name hat gewechselt, weil „Middleware” nach dem Ort für Anmeldung und Sitzungsverwaltung klang — genau dorthin gehört sie nicht.

AufgabeGehört in den Proxy
Nichtangemeldete auf die Anmeldung lenkenja, optimistisch
Kopfzeilen setzen, A-B-Test umschreibenja
Sitzung in der Datenbank prüfennein, zu langsam
Rechte je Datensatz entscheidennein, falscher Ort
Daten ladennein

Der Proxy ist eine Weiche, keine Wache. Er läuft bei jeder Anfrage, auch bei jedem Prefetch — eine Datenbankabfrage darin vervielfacht die Last unbemerkt.

Modul: Route Handlers und Proxy

Datenbank und Zugriffs-Layer

PrismaDrizzle
Schemaquelleeigene SpracheTypeScript
Nähe zu SQLabstrahiertnah
Generierungsschrittjanein
Einstiegleichterdirekter
Bundle-Größegrößerkleiner

Der Datenzugriffs-Layer führt Abfrage, Rechteprüfung und Zuschnitt an einem Ort zusammen — dort ist Sicherheit prüfbar statt über Seiten verstreut:

import 'server-only'
export async function getRechnung(id: string) {
  const sitzung = await verifySession()
  const r = await ladeRechnung(id)
  if (r.gastId !== sitzung.userId && sitzung.rolle !== 'verwaltung') forbidden()
  return { nummer: r.nummer, betrag: r.betrag }     // schmal zurückgeben
}
AbfrageCacheForm
Tariflistejageteilt
freie Liegeplätzejageteilt, kurz
eigene Buchungennur privatim Browser
personenbezogene Datensätzeneinungecacht

Die Frage lautet immer: Sähen zwei Nutzer dasselbe Ergebnis?

serverloseigener Node-Prozess
Instanzenviele, kurzlebigwenige, langlebig
Verbindungenje Aufruf neuwiederverwendet
Poolzwingend, externim Prozess

Zwei Dinge gehören in die Datenbank, nicht in den Code: die Ausschlussbedingung gegen doppelte Belegung — zwei gleichzeitige Anfragen umgehen jede Prüfung, die nur in der Anwendung steht — und Geldbeträge als exakter Typ statt als Fließkommazahl.

Modul: Datenbankzugriff mit PostgreSQL

Anmeldung und Sitzungen

EbeneFrageMittel
Authentifizierungwer bist duAnmeldeformular
Sitzungbist du noch daCookie
Autorisierungwas darfst duRolle und Besitz
(await cookies()).set('sitzung', wert, {
  httpOnly: true, secure: true, sameSite: 'lax', expires: bis, path: '/',
})
OptionVerhindert
httpOnlyAuslesen durch JavaScript im Browser
secureÜbertragung über unverschlüsselte Verbindungen
sameSiteMitsenden bei fremden Seitenaufrufen
expiresunbegrenzt gültige Sitzungen
pathGeltung außerhalb des vorgesehenen Bereichs

Fehlt eine davon, ist die Sitzung auf eine konkrete Weise angreifbar. Ins Cookie gehören keine Adressen, Telefonnummern oder Rechnungsdaten — es wird vom Nutzer transportiert und ist nie vertrauenswürdig.

zustandslosSitzung in der Datenbank
Aufwandgeringhöher
sofort widerrufbarneinja
Abfrage je Prüfungneinja
aktive Geräte sichtbarneinja
AufgabeSelbst nötig
Soziale Anmeldungerheblicher Aufwand
Mehrfaktorerheblicher Aufwand
Passwort vergessenmittlerer Aufwand
Sitzungsverwaltungüberschaubar

Die unteren Zeilen sind machbar, die oberen selten selbst zu rechtfertigen — und ein Wechsel später ist teuer. Ein Dienst als Anbieter bedeutet dabei Auftragsverarbeitung und meist Drittlandtransfer.

Eine zustandslose Sitzung gilt bis zu ihrem Ablauf, auch nach einer Sperrung — für Verwaltungsrollen ist die rechte Spalte oft richtig. Die Anmeldeaktion gibt für „Nutzer unbekannt” und „Passwort falsch” dieselbe Meldung zurück, sonst verrät sie, welche Konten existieren.

Modul: Authentifizierung und Sessions

Autorisierung: wo eine Prüfung wirkt

OrtWirktReicht
Proxyjede Anfragenein, optimistisch
Layouteinmal je Zweignein
Seiteje Aufrufnur für die Ansicht
Zugriffs-Layerje Abfrageja

Nur die unterste Zeile deckt Seite, Aktion und Endpunkt gleichzeitig ab. Und Rolle allein genügt nicht — die zweite Prüfung, der Besitz, ist die, die am häufigsten fehlt:

if (!sitzung) unauthorized()
if (b.gastId !== sitzung.userId && sitzung.rolle !== 'verwaltung') forbidden()

Übertragungsobjekte statt Datensätze halten das über Umbauten hinweg: Was im DTO nicht steht, kann später nicht versehentlich sichtbar werden — ein neues Feld in der Datenbank landet sonst automatisch im Browser.

Bei Server Actions eingebautBleibt deine Aufgabe
verschlüsselte Aktions-KennungenAnmeldung prüfen
ungenutzte Aktionen entferntBerechtigung prüfen
Ursprung gegen Host geprüftEingaben validieren
nur POST erlaubtRückgabe zuschneiden

Die linke Spalte senkt das Risiko, die rechte beseitigt es. Hinter einem Reverse Proxy braucht die Ursprungsprüfung eine Liste erlaubter Ursprünge; ab der zweiten Instanz muss der Schlüssel fest vorgegeben sein:

openssl rand -base64 32     # → NEXT_SERVER_ACTIONS_ENCRYPTION_KEY

Modul: Autorisierung und Datensicherheit

Testen

EbeneWerkzeugGegenstand
EinheitVitestZugriffs-Layer, Aktionen, Schemas
KomponenteVitestClient-Komponenten mit Zustand
DurchstichPlaywrightangemeldet buchen und sehen
RechtePlaywrightpositiv und negativ je Rolle

Async Server Components fehlen in dieser Liste bewusst. Eine Server Action testet man, indem man sie als Funktion aufruft — nicht über ein Formular:

await expect(getBuchung('b-2', { userId: 'g-1', rolle: 'gast' })).rejects.toThrow()

Der negative Fall ist der wichtigere: Er prüft die Absicherung, und er überlebt Umbauten der Oberfläche, weil er eine fachliche Zusage beschreibt. Abgefragt wird über die Rolle (getByRole), nicht über CSS-Klassen.

import { instant } from '@next/playwright'
await instant(page, async () => { /* muss ohne Warten auf das Netz sichtbar sein */ })

Das macht aus dem Geschwindigkeitsversprechen der sofortigen Navigation eine prüfbare Zusage — eine verschobene Suspense-Grenze schlägt sonst nirgends fehl. Was den Merge nicht blockiert, wird nach zwei Wochen ignoriert.

Modul: Testen mit Vitest und Playwright

Produktionsreife und Selbsthosten

OrtEnthält
appRouten und Konventionsdateien
dataZugriffs-Layer, server-only, mit Rechteprüfung
libHilfsfunktionen ohne Fachbezug
uigeteilte Komponenten, Client-Grenzen
e2eDurchstiche und Rollentests

Image, Font und Script sind Standardbausteine, keine Zusatzpakete — sie übernehmen Bildgrößen und moderne Formate, Selbstauslieferung der Schriften ohne Layoutsprung und das verzögerte Laden fremder Skripte.

Was Vercel übernimmtBedeutet beim Selbsthosten
CDN vor der statischen Hülleselbst einzurichten
Cache-Speicher für ISRCache-Handler nötig
Bildoptimierungeigener Dienst oder Konfiguration
Skew-Schutz bei RolloutsAuslieferungskennung setzen
Selbstbau-AufgabeSonst passiert
Cache-Handlerjede Instanz zeigt anderen Stand
VerschlüsselungsschlüsselAktion nicht gefunden
AuslieferungskennungFehler bei rollenden Rollouts
Pufferung abschaltenStreaming kommt nicht an
VerbindungspoolDatenbankgrenze erreicht

Alle fünf treten erst mit der zweiten Instanz auf, nie beim lokalen Test. Cache Components funktioniert dabei selbst gehostet — es ist keine Plattformfunktion.

Modul: Produktionsreife und Deployment

Agentische Entwicklung

Der Trainingsstand der Modelle liegt vor Next.js 16. Die vier häufigsten Vorschläge sind plausibel formuliert und alle vier falsch:

Vorschlag des AgentenHeute richtig
Pages Router, getServerSidePropsApp Router, await in der Komponente
middleware.tsproxy.ts
fetch mit revalidate-Optionuse cache mit cacheLife
Datenladen im useEffectServer Component

Gegenmittel ist der von next dev gepflegte Block in AGENTS.md, der auf die mitgelieferte Doku unter node_modules/next/dist/docs/ verweist. Eigene Projektregeln gehören außerhalb der Markierungen — dort bleiben sie erhalten.

Typischer Fehler im generierten CodeAus welchem Modul
Client-Grenze zu weit obenServer und Client Components
Validierung in der Aktion fehltFormulare und Server Actions
Cache-Tag nicht invalidiertCaching mit Cache Components
Besitzprüfung fehltAutorisierung und Datensicherheit
AufgabeEignungGrund
Migration, RefactoringhochSollzustand klar
Tests schreibenhochsofort prüfbar
Dokumentationhochleicht zu beurteilen
ArchitekturentwurfmittelAbwägung nötig
Fachliche Regeln erfindenniedrigSollzustand unklar

Die untere Zeile ist die, bei der Agenten am überzeugendsten danebenliegen.

Modul: Agentische Entwicklung mit Next.js

Typische Fallen

  • Eine exportierte Server Action ist über einen direkten POST erreichbar, auch ohne Verlinkung. Anmeldung und Berechtigung gehören in die Aktion selbst — eine Prüfung auf der Seite deckt die dort definierte Aktion nicht mit ab.
  • Ein zurückgegebener Datenbanksatz veröffentlicht interne Felder. Rückgabewerte werden serialisiert und sind im Browser lesbar.
  • redirect nach der Invalidierung aufrufen, nicht davor — danach läuft kein Code mehr. Ein Fehler in der Aktion ohne Rückgabe lässt das Formular stumm.
  • Wer den Proxy als einzige Absicherung nutzt, hat keine. Ein Cookie kann veraltet sein; die optimistische Prüfung ist kein Beweis.
  • Beim Löschen und Ändern wird der Besitz öfter vergessen als beim Lesen. Eine Kennung aus der URL ist Benutzereingabe, kein Beweis für Zugehörigkeit.
  • Eine Rolle im Cookie ist eine Behauptung, kein Nachweis — und eine gültige Sitzung sagt nichts über Berechtigungen aus.
  • Cookies lassen sich nicht beim Rendern setzen. Anmelden und Abmelden sind Aktionen; bei Datenbanksitzungen genügt das Löschen des Cookies nicht.
  • Der ORM ersetzt kein Verständnis der erzeugten Abfragen — naiv geladene Beziehungen erzeugen das klassische N-plus-1-Problem.
  • Ein Pool im Anwendungsprozess hilft serverlos nicht, er muss davor liegen. Der Fehler tritt erst unter Last auf, nicht in der Entwicklung.
  • Eine Vorschau-Auslieferung mit Produktionsdaten ist ein verbreiteter Fehler; Umgebungsvariablen je Umgebung trennen.
  • Feste Wartezeiten in Playwright machen Tests langsam und trotzdem unzuverlässig; ohne zurückgesetzten Datenstand hängt der zweite Lauf vom ersten ab.
  • Generierter Code, der läuft, ist damit nicht richtig. Agenten prüfen gern den positiven Fall und lassen den negativen weg.

Zum Seminar Next.js 16 Full-Stack-Entwicklung mit React und TypeScript