Next.js in Produktion
Verwenden für jeglichen Code in einer Next.js 16 App Router App. Sicherheitsspezifische Regeln (Authentifizierung in Actions, CSP, Header, Taint) befinden sich in nextjs-security; Container-Regeln in docker-nextjs. Das integrierte next-best-practices-Skill von Claude Code behandelt allgemeine Dateikonventionen, Metadaten, Bild- und Schriftoptimierung ausführlich; dieses Skill fügt nur hinzu, was für unsere Art zu bauen und auszuliefern spezifisch ist.
Basislinie: Next 16.4, React 19.3, Turbopack (Standard für dev und build), cacheComponents: true mit partialPrefetching: true, output: "standalone", typedRoutes: true. Die next.config.ts der Vorlage ist die Quelle der Wahrheit.
Regeln
-
Server Components standardmäßig; "use client" nur für State, Effekte, Browser-APIs oder Event-Handler. Setze die Direktive auf das kleinste Blatt, das sie benötigt. Füge sie nicht hinzu, weil eine Datei nuvui importiert: Die statebehafteten Teile von nuvui tragen ihre eigene Direktive (siehe das
nuvuiSkill,rules/server-components.md).// Falsch: ganze Seite ist client wegen eines Handlers "use client"; export default function Page() { … <Select onValueChange={…} /> … } // Richtig: die Seite bleibt auf dem Server, der Handler lebt in einem Blatt export default async function Page() { const people = await getPeople(); return <PeopleFilter people={people} />; // people-filter.tsx hat "use client" }Props, die in eine Client-Komponente übergeben werden, müssen serialisierbar sein: keine Funktionen (außer Server Actions), keine Klasseninstanzen.
-
Fetch auf dem Server, in der Komponente, die die Daten benötigt. Kein
useEffect-Fetching, kein Route Handler nur um die eigene Seite zu füttern. Deduplizieren pro Request mit Reactscache(); über Requests hinweg cachen mit Regel 3. -
Cachen mit "use cache" +
cacheLife+cacheTag; alles andere ist dynamisch. Mit Cache Components wird nichts gecached, es sei denn, du sagst es, undfetchhat keinen impliziten Cache.import { cacheLife, cacheTag } from "next/cache"; export async function getInvoice(id: InvoiceId) { "use cache"; cacheLife("minutes"); cacheTag(`invoice:${id}`); return db.invoice.findUnique({ where: { id } }); }Verwende nicht die alte Segment-Konfiguration (
export const dynamic,revalidate,fetchCache): Sie verursacht Fehler unter Cache Components. Argumente und geschlossene Werte werden Teil des Cache-Keys, also übergib IDs, nicht ganze Request-Objekte. Details, die Profile und "use cache: private": references/caching.md. -
Invalidieren mit
updateTagin Server Actions,revalidateTag(tag, "max")anderswo.updateTag(tag)ermöglicht Read-Your-Own-Writes und funktioniert nur innerhalb einer Server Action. Von einem Webhook oder Route Handler aus verwenderevalidateTag(tag, "max")(Stale-While-Revalidate) oderrevalidateTag(tag, { expire: 0 }). Die Ein-Argument-VarianterevalidateTag(tag)ist veraltet. -
Kapsle anfragezeitliche Daten in
<Suspense>, sonst schlägt der Build fehl.cookies(),headers(),searchParams,connection()und ungecachte I/O außerhalb einer Suspense-Grenze stoppen das Vorrendern mit "uncached or runtime data during prerendering". Gib ihm eine Grenze mit einem Skeleton-Fallback, cache es oder (nur Root-Layout, siehe Regel 14) markiere die Route als blockierend mitexport const instant = false. Alleparams,searchParams,cookies(),headers()sind asynchron:awaitsie. -
Jede Server Action validiert ihre Eingabe und überprüft die Autorisierung selbst. Eine Server Action ist ein öffentlicher POST-Endpunkt, egal auf welcher Seite sie geschrieben ist. Parse
FormDatamit dem Schema austypescript-production(valibot), dann autorisiere, dann mutiere, dannupdateTag. Gib ein typisiertes Ergebnis für erwartete Fehler zurück; wirf nur für Bugs. Das vollständige Regelwerk befindet sich innextjs-security."use server"; export async function renameProject(_: State, form: FormData): Promise<State> { const session = await requireSession(); // 401 wenn abwesend const input = v.safeParse(RenameProject, Object.fromEntries(form)); if (!input.success) return { ok: false, errors: flatten(input.issues) }; await assertCanEdit(session, input.output.projectId); // 403 wenn nicht erlaubt await db.project.update(…); updateTag(`project:${input.output.projectId}`); return { ok: true }; } -
Route Handler sind für andere Clients (Webhooks, Mobil, Health), nicht für deine eigenen Seiten. Lege sie unter
app/api/. Validiere den Body wie eine Action, gibResponse.json(...)zurück und setze Statuscodes bewusst. -
Jede App hat
GET /api/health, dynamisch und abhängigkeitsfrei. DockerHEALTHCHECKund die Kubernetes-Probes treffen darauf. Es muss antworten, ohne die Datenbank zu berühren, damit eine langsame Abhängigkeit nicht jeden Pod neustartet.await connection()verhindert, dass es zu einer statischen Datei vorgerendert wird. Es befindet sich in der Vorlage untersrc/app/api/health/route.ts;proxy.tsschließt/api/healthvon seinem Matcher aus. -
error.tsx,loading.tsxundnot-found.tsxim Root und pro Segment, wo sich die UX unterscheidet.error.tsxist eine Client-Komponente; logge durchonRequestErrorvoninstrumentation.ts, zeige einen Retry mitreset, niemals die Fehlermeldung. RufenotFound()für fehlende Datensätze auf, anstatt einen leeren Zustand zu rendern. -
NEXT_PUBLIC_*wird zur Build-Zeit eingebrannt; alles andere wird zur Laufzeit auf dem Server gelesen. Ein Image wird unverändert von Main auf Production befördert, daher ist einNEXT_PUBLIC_-Wert in jeder Umgebung gleich. Gib dort nur Build-Zeit-Konstanten an (App-Name, ein öffentlicher Sentry DSN, der überall gleich ist). Umgebungsspezifische Konfiguration (API-URLs, Feature-Flags, Secrets) ist serverseitigesprocess.env.X, gelesen in einer Server Component oder Action, und wird bei Bedarf als Props an den Client übergeben. Validiere Env einmal insrc/env.ts(valibot) und importiere von dort. Verwende niemals denenv-Schlüssel innext.config.ts: Er wird in Client-Bundles inline eingefügt. -
Markiere Server-Only-Module mit
import "server-only". Alles, was Secrets liest, mit der DB spricht oder ein SDK mit Anmeldedaten importiert. Ein Client-Import davon führt dann zu einem Build-Fehler anstatt zu einem Leak. -
after()für Arbeit, die die Antwort nicht verzögern sollte (Audit-Logs, Analytics, Cache-Warm-up). Lesecookies()/headers()vor dem Aufruf vonafter, nicht darin. Es läuft im selben Prozess, daher braucht der Pod seine SIGTERM-Grace-Periode (das Chart setztterminationGracePeriodSeconds). -
Instrumentieren mit
src/instrumentation.ts:registerOTel+onRequestError. OpenTelemetry via@vercel/otel, Exporter konfiguriert durch die standardmäßigenOTEL_EXPORTER_OTLP_*-Umgebungsvariablen zur Laufzeit. Siehe references/observability.md. -
Behalte
output: "standalone"und importiere niemals von außerhalb des Workspace. Der Docker-Runner kopiert nur.next/standalone,.next/staticundpublic. Im Monorepo landetserver.jsunter.next/standalone/apps/<app>/server.js; das Workspace-Root wird auspnpm-lock.yamlgefunden, also setzeoutputFileTracingRootoderturbopack.rootnur, wenn der Build warnt. Self-Hosting-Details (Multi-Replica, Server Action Keys,deploymentId): references/self-hosting.md. -
Das Root-Layout liest den CSP-Nonce, daher rendert die App pro Request. Die
proxy.tsder Vorlage setzt ein Nonce-basiertes CSP undsrc/app/layout.tsxhatexport const instant = false. Das ist ein bewusster Kompromiss: Nonces und vorgerenderte statische Shells können nicht koexistieren, und für eingeloggte Business-Apps gewinnt ein striktes CSP. Daten-Caching mit "use cache" funktioniert weiterhin. Entferne keine der beiden Hälften, ohnenextjs-securityRegel 4 zu lesen. -
Styles: globales BEM SCSS, von der Komponente importiert. Turbopack kompiliert Sass mit dem
sass-Paket;sassOptions.loadPaths: ["src/styles"]macht@use "abstracts"aus jeder Tiefe funktionsfähig. Keine~-Imports, keinesassOptions.functions. Regeln inscss-bem. -
Bilder durch
next/image, Schriften durchnext/font.sharpkommt mit Next für den Optimierer; das Runner-Image benötigt kein zusätzliches Paket. Remote-Bilder benötigenimages.remotePatternsmit einem exakten Hostnamen. -
Überprüfe die Bundle-Größe mit
pnpm next analyze(Turbopacks Analyzer;@next/bundle-analyzerist nur für Webpack). Lade schwere Client-Widgets (nuvui Editor, Karten, Diagramme unter dem Fold) lazy mitnext/dynamic. -
Verwende typisierte Routen und generierte Typen.
typedRoutes: trueüberprüft<Link href>;PageProps<"/invoices/[id]">undLayoutPropssind global nachnext typegen(wird automatisch vonnext dev/next buildausgeführt). Schreibeparams-Typen nicht von Hand. -
Kein
next lint. Es wurde in 16 entfernt. Biome lintet (siehebiome); TypeScript überprüft mittsc --noEmitpro Paket.
Überprüfen
pnpm typecheck
pnpm lint
pnpm build # turbo run build: schlägt fehl bei Prerender-Fehlern, Typfehlern, Sass-Fehlern
pnpm --filter <app> start & # dann:
curl -fsS localhost:3000/api/health
Quellen
- https://nextjs.org/docs/app/guides/upgrading/version-16
- https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents
- https://nextjs.org/docs/app/guides/migrating-to-cache-components
- https://nextjs.org/docs/app/api-reference/directives/use-cache
- https://nextjs.org/docs/app/api-reference/functions/revalidateTag
- https://nextjs.org/docs/app/api-reference/functions/updateTag
- https://nextjs.org/docs/messages/blocking-prerender-dynamic
- https://nextjs.org/docs/app/api-reference/functions/after
- https://nextjs.org/docs/app/guides/open-telemetry
- https://nextjs.org/docs/app/api-reference/config/next-config-js/output
- https://nextjs.org/docs/app/guides/self-hosting
- https://nextjs.org/docs/app/guides/environment-variables
- https://nextjs.org/docs/app/guides/sass
- https://nextjs.org/docs/app/guides/package-bundling
- https://nextjs.org/docs/app/api-reference/file-conventions/proxy