Server Routes, Composables und Nitro richtig einsetzen
Nuxt 3 ist mehr als ein Vue-3-Framework mit File-Based-Routing. Die Nitro Engine, Server Routes, Auto-Import und die klare Trennung von Server- und Client-Schichten sind ein durchdachtes Architekturmodell — das nur dann seine Stärken zeigt, wenn man versteht, warum es so gebaut ist.
Inhaltsverzeichnis
- 1. Die drei Schichten der Nuxt 3 Architektur
- 2. Nitro: der universelle Server hinter Nuxt 3
- 3. Server Routes: leichtgewichtiger API-Layer in /server/api
- 4. Composables und Auto-Import: wie Nuxt 3 das löst
- 5. useFetch vs. $fetch: wann welches Werkzeug?
- 6. Middleware in Nuxt 3: Route Guards und Server Middleware
- 7. Plugins und App-Lifecycle in Nuxt 3
- 8. Server- vs. Client-only Code: .server.ts und .client.ts
- 9. Nuxt 3 Rendering-Modi im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Die drei Schichten der Nuxt 3 Architektur
Wer Nuxt 3 nur als „Vue 3 mit File-Based-Routing" versteht, unterschätzt, was das Framework wirklich leistet. Das Framework-Design folgt einem klaren Schichtenmodell, bei dem jede Schicht eine eigene Laufzeitumgebung, eigene APIs und eigene Zuständigkeiten hat. Diese Trennung ist kein Zufall, sondern bewusste Architekturentscheidung — sie erlaubt Nuxt 3, dieselbe Codebasis auf einem klassischen Node.js-Server, auf serverless Functions und auf Edge-Runtimes wie Cloudflare Workers zu betreiben.
Die Nuxt 3 Architektur besteht aus drei klar getrennten Schichten, die unterschiedliche Verantwortlichkeiten tragen. Die erste Schicht ist die Nitro-Engine — der universelle Server, der Nuxt 3 von einem einfachen Vue-3-Framework unterscheidet. Die zweite Schicht ist die Vue-3-Client-Anwendung — Pages, Layouts, Komponenten und Composables, die im Browser rendern und per SSR vorgerendert werden. Die dritte Schicht ist der Build-Layer — Vite als Entwicklungsserver und Rollup als Produktions-Bundler, koordiniert durch Nuxt selbst.
Jede dieser drei Schichten hat eine strikt abgegrenzte Laufzeitumgebung: Nitro kennt keinen DOM, Vue-Komponenten dürfen nicht direkt auf Datenbanken zugreifen, und der Build-Layer ist zur Laufzeit komplett unsichtbar. Diese Grenzen zu kennen und zu respektieren ist die Grundvoraussetzung für eine wartbare Nuxt 3 Architektur.
Das Verständnis dieser drei Schichten ist entscheidend, um die richtigen Nuxt-3-Werkzeuge für jede Aufgabe zu wählen. Datenbankzugriffe, API-Authentifizierung und sensible Business-Logik gehören in die Nitro-Schicht — Server Routes und Server Middleware. Interaktive UI-Komponenten, reaktive Zustände und Nutzerinteraktionen gehören in die Vue-Schicht. Build-Konfigurationen, Module und Layer-Extensions gehören in den Build-Layer. Wer Code in der falschen Schicht platziert — etwa Datenbankzugriffe direkt in einer Vue-Komponente ohne Server Route — produziert Sicherheitslücken oder Code, der im SSR-Kontext fehlschlägt.
2. Nitro: der universelle Server hinter Nuxt 3
Nitro ist das Herzstück der Nuxt 3 Architektur und gleichzeitig das am wenigsten verstandene Element. Nitro ist ein eigenständiges Framework für serverseitige Anwendungen, das mit dem H3-Mikro-Framework aufgebaut ist. Es kompiliert den Server-Code zu einem einzigen, deploymentfähigen Bundle, das auf verschiedenen Plattformen läuft: Node.js, Edge Runtimes wie Cloudflare Workers, Vercel Edge, Netlify Functions, und mehr. Dieses universelle Deployment-Modell ist der Grund, warum eine Nuxt-3-Anwendung ohne Konfigurationsänderung von einem klassischen VPS zu einem CDN-Edge-Netzwerk verschoben werden kann.
Nitro bietet eingebautes Caching mit konfigurierbaren Cache-Strategien, ein Event-System basierend auf H3, Server-seitiges Rendering und die Ausführung von Server Routes. Das Nitro-Cache-System ist besonders mächtig: Server Routes können ihre Responses mit einer Zeile Konfiguration cachen — nach Zeit, nach Request-Parameter oder nach Custom-Keys. Dieses Caching findet auf dem Server statt, bevor der Request die Vue-Anwendung überhaupt erreicht, und reduziert die Server-Last bei häufig angeforderten Daten drastisch. Der folgende Abschnitt zeigt, wie Server Routes in der Praxis aufgebaut werden.
3. Server Routes: leichtgewichtiger API-Layer in /server/api
Nuxt 3 Server Routes sind Handler-Funktionen im Verzeichnis /server/api/ und /server/routes/, die automatisch zu API-Endpunkten werden. Sie laufen ausschließlich auf dem Server, haben Zugriff auf die Nuxt-Server-Konfiguration und können direkt auf Datenbanken, externe APIs und Environment-Variablen zugreifen — ohne dass der Client-Code jemals den Zugriffs-Token sieht. Das ist der fundamentale Sicherheitsvorteil gegenüber direkten API-Aufrufen aus dem Client.
Die Datei-zu-Route-Konvention der Nuxt 3 Server Routes ist intuitiv: /server/api/products.get.ts wird zu GET /api/products, /server/api/products.post.ts zu POST /api/products, und /server/api/products/[id].ts zu /api/products/:id für alle HTTP-Methoden. Das Suffix im Dateinamen (.get, .post, .put, .delete) beschränkt den Handler auf eine HTTP-Methode. Handler ohne Suffix akzeptieren alle Methoden und können intern mit getMethod(event) differenzieren.
// server/api/products/[id].get.ts
// Nuxt 3 Server Route — runs on server only, never in the browser bundle
import { z } from 'zod'
// Define route-level cache: response cached for 5 minutes by product ID
export default defineCachedEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
// Input validation with zod — rejects invalid IDs before DB query
const schema = z.string().uuid()
const parsed = schema.safeParse(id)
if (!parsed.success) {
throw createError({ statusCode: 400, message: 'Invalid product ID format' })
}
// DB access — safe here because this code never runs in the browser
const product = await db.products.findUnique({ where: { id: parsed.data } })
if (!product) {
throw createError({ statusCode: 404, message: 'Product not found' })
}
return product
}, {
maxAge: 60 * 5, // cache for 5 minutes
getKey: (event) => `product:${getRouterParam(event, 'id')}`,
})
4. Composables und Auto-Import: wie Nuxt 3 das löst
Das Auto-Import-System von Nuxt 3 ist eines der kontroversesten Features — und eines der durchdachtesten, wenn man versteht, wie es funktioniert. Dateien in /composables/, /utils/ und /components/ werden automatisch in jeden Component-Scope importiert, ohne dass explizite Import-Statements nötig sind. Nuxt analysiert den Code statisch zur Build-Zeit und fügt die fehlenden Imports automatisch ein. Das bedeutet: Kein IDE-Confusion, kein versteckter globaler State — nur compile-time-generierte Imports.
Eigene Composables in /composables/ werden ebenfalls auto-importiert und können die eingebauten Nuxt-3-Composables wie useFetch, useState und useRouter erweitern oder wrappen. Der Unterschied zu Vue-Composables außerhalb von Nuxt: Nuxt-Composables haben Zugriff auf den SSR-Kontext und können Server-seitige Daten mit Client-seitigem Hydrations-State verbinden. Ein useProducts-Composable, das intern useFetch verwendet, funktioniert sowohl beim initialen Server-Render als auch bei nachfolgenden Client-seitigen Navigationen — ohne Code-Duplikation oder Sonderfälle.
// composables/useProducts.ts
// Auto-imported by Nuxt — works on server (SSR) and client (navigation)
export function useProducts(categoryId?: Ref<string>) {
// useFetch is SSR-aware: fetches on server for initial render,
// then uses cached data for client-side hydration
const { data: products, status, error, refresh } = useFetch(
() => `/api/products${categoryId?.value ? `?category=${categoryId.value}` : ''}`,
{
key: () => `products-${categoryId?.value ?? 'all'}`, // deduplicate requests with same key
watch: [categoryId], // auto-refetch when categoryId changes
transform: (data) => data.map(normalizeProduct), // transform before caching
}
)
const isLoading = computed(() => status.value === 'pending')
const isEmpty = computed(() => !isLoading.value && products.value?.length === 0)
return { products, isLoading, isEmpty, error, refresh }
}
5. useFetch vs. $fetch: wann welches Werkzeug?
Die häufigste Verwirrung in der Nuxt 3 Architektur: Wann useFetch, wann useAsyncData, wann $fetch? Die Antwort hängt vom Kontext ab. useFetch ist die Komposition aus useAsyncData und $fetch — es kombiniert Datenabruf, Caching, SSR-Hydration und reaktive URL-Verknüpfung in einem Aufruf. Es ist die richtige Wahl für Daten, die beim initialen Page-Render benötigt werden und Teil des SSR-Outputs sein sollen.
$fetch ist das rohe ofetch-basierte HTTP-Client-Objekt ohne SSR-Bewusstsein. Es ist die richtige Wahl für programmatische API-Calls, die nicht beim Page-Render ausgelöst werden — Button-Clicks, Form-Submissions, Mutations. useFetch in einer Button-Click-Handler zu verwenden ist ein häufiger Fehler: Es erstellt eine reaktive Binding, die beim Komponenten-Unmount nicht automatisch abgebrochen wird, und teilt den Request-State global mit allen Instanzen desselben key. Für imperative API-Calls ist $fetch das semantisch korrekte Werkzeug.
6. Middleware in Nuxt 3: Route Guards und Server Middleware
Nuxt 3 hat zwei verschiedene Middleware-Konzepte, die oft verwechselt werden. Route-Middleware läuft im Client-Router — JavaScript-Funktionen in /middleware/, die vor jeder Navigation ausgeführt werden. Sie dienen als Route Guards: Authentifizierung prüfen, Redirects ausführen, Analytics-Events senden. Server-Middleware läuft in der Nitro-Engine — Funktionen in /server/middleware/, die jeden HTTP-Request abfangen, bevor er einen Handler erreicht. Sie dienen für CORS-Headers, Rate-Limiting, Request-Logging und JWT-Validierung.
Der entscheidende Unterschied: Route-Middleware hat Zugriff auf Nuxt-State und Vue-Router, läuft aber nur im Browser (außer bei serverseitig gerendertem initialen Request). Server-Middleware hat Zugriff auf den vollständigen HTTP-Request und alle Server-Ressourcen, läuft aber komplett unabhängig vom Vue-Client-Code. Authentifizierungs-Logic, die auf Cookies oder JWT-Tokens basiert, gehört in die Server-Middleware — dort kann sie nicht vom Client umgangen werden. Route-Middleware ist die zweite Sicherheitsschicht für UX-Feedback, nicht die einzige.
7. Plugins und App-Lifecycle in Nuxt 3
Nuxt-3-Plugins in /plugins/ werden automatisch beim App-Start ausgeführt und können den nuxtApp-Kontext erweitern. Sie sind das richtige Werkzeug, um globale Bibliotheken zu initialisieren, Direktiven zu registrieren und globale State-Provider einzurichten. Das Suffix .server.ts beschränkt ein Plugin auf den Server-Render, .client.ts auf den Browser. Ohne Suffix läuft das Plugin auf beiden Seiten.
Ein wichtiges Detail der Nuxt 3 Architektur: Plugins können async sein. Ein async Plugin pausiert den Nuxt-Startup-Prozess, bis das Plugin aufgelöst hat. Das ist sinnvoll für Plugins, die Konfiguration laden oder externe Services initialisieren müssen. Aber es verzögert den First-Render — async Plugins sollten so kurz wie möglich gehalten werden und keine Netzwerkrequests enthalten, die auch later geladen werden könnten.
8. Server- vs. Client-only Code: .server.ts und .client.ts
Das Suffix-System von Nuxt 3 für Server- und Client-only-Dateien ist ein elegantes Architekturmuster. Eine Datei analytics.client.ts wird nur in das Client-Bundle eingebunden — sie schlägt auf dem Server nicht fehl, weil sie dort einfach nicht existiert. Eine Datei dbClient.server.ts erscheint nie im Client-Bundle — der Datenbankcode und alle seine Imports sind physisch ausgeschlossen. Das verhindert Bundle-Aufblähung durch Server-only-Bibliotheken und verhindert, dass sensible Server-Code-Teile im Browser inspizierbar sind.
Für Komponenten funktioniert dasselbe Muster: Eine HeavyChart.client.vue-Komponente wird nur im Browser gerendert, nie auf dem Server. Der Server liefert einen Placeholder, der Browser ersetzt ihn nach der Hydration. Das ist die empfohlene Lösung für Komponenten, die Browser-APIs (window, document, Canvas) benötigen — statt des häufigen Fehlers, window-Zugriffe manuell mit if (process.client) zu wrappen.
9. Nuxt 3 Rendering-Modi im Vergleich
Nuxt 3 unterstützt mehrere Rendering-Modi, die pro Route konfiguriert werden können — eine Funktion, die die Nuxt 3 Architektur flexibel genug für hybride Anwendungen macht. Die folgende Tabelle zeigt die Unterschiede und wann welcher Modus sinnvoll ist.
| Modus | Nuxt-Konfiguration | Einsatzgebiet | Caching |
|---|---|---|---|
| SSR (Standard) | ssr: true | Dynamische Seiten, SEO, Auth | Nitro-Cache pro Response |
| SSG / Pre-rendering | nitro.prerender | Blogs, Docs, statische Inhalte | CDN-Cache (statische Dateien) |
| Client-only (SPA) | ssr: false | Admin-UIs, Dashboard, kein SEO nötig | Browser-Cache, kein Server |
| Hybrid Rendering | routeRules per Route | Marketing-Seiten + App gemischt | Pro Route konfigurierbar |
| ISR (Incremental) | routeRules.isr: 60 | Produkt-/Blog-Seiten mit Update-Bedarf | Stale-While-Revalidate |
Das Hybrid-Rendering-Modell ist eine der stärksten Funktionen der Nuxt 3 Architektur. Mit routeRules in der nuxt.config.ts lassen sich verschiedene Caching- und Rendering-Strategien pro URL-Pattern konfigurieren: Marketing-Seiten werden statisch vorgerendert, Produktseiten mit ISR aktualisiert, und die Checkout-Route läuft vollständig SSR ohne Cache. Diese Granularität erlaubt es, Performance-Optimierungen exakt dort anzusetzen, wo sie den größten Effekt haben.
Mironsoft
Nuxt 3 Architektur, Server Routes und Full-Stack-Vue-Entwicklung
Nuxt 3 Projekte, die architektonisch solide aufgebaut sind?
Wir bauen Nuxt-3-Anwendungen mit klarer Schichtentrennung, Nitro-Caching, typisieren Server Routes und eingebetteten Composables — skalierbar für Teams und Deployment-Anforderungen.
Server Routes Design
Typisierte API-Layer mit Nitro-Caching, Validierung und Auth-Middleware
Composable-Architektur
useFetch-Wrapper, Auto-Import-Strategie und SSR-Hydration sauber umgesetzt
Hybrid Rendering
routeRules für SSR, SSG, ISR und Client-only je nach Seitentyp konfiguriert
10. Zusammenfassung
Die Nuxt 3 Architektur ist ein durchdachtes Schichtenmodell: Nitro als universeller Server mit eingebautem Caching und Edge-Deployment-Fähigkeit, Server Routes als sicherer API-Layer ohne separates Backend-Repository, Composables mit Auto-Import als klare Abstraktionsschicht für reaktive Logik, und ein flexibles Rendering-Modell von SSR über SSG bis zu Hybrid-Routing. Das Verständnis dieser Schichten ist die Voraussetzung, um Nuxt-3-Entscheidungen zu treffen, die mit wachsendem Projekt nicht zur Last werden.
Die häufigen Fehler in Nuxt-3-Projekten entstehen durch Schichtenverwirrung: Datenbankcode in Vue-Komponenten, useFetch in Event-Handlern, Authentifizierung nur in Route-Middleware statt auch in Server-Middleware, und .server.ts-Dateien, die Browser-APIs importieren. Mit einem klaren Bild der drei Schichten und ihrer Zuständigkeiten lassen sich diese Fehler systematisch vermeiden — von der ersten Zeile Code an.
Der langfristige Architektur-Vorteil von Nuxt 3 zeigt sich besonders dann, wenn Projekte wachsen: Neue Server Routes lassen sich hinzufügen, ohne bestehende Client-Komponenten anzufassen. Neue Composables werden automatisch importiert, ohne Build-Konfigurationen zu ändern. Das Rendering-Modell kann pro Route angepasst werden, wenn sich Anforderungen ändern — ohne Refactoring der gesamten Anwendung. Diese Flexibilität ist das Ergebnis der klaren Schichtentrennung, die Nuxt 3 von Beginn an als Designprinzip verfolgt.
Nuxt 3 Architektur — Das Wichtigste auf einen Blick
Nitro Engine
Universeller Server hinter Nuxt 3. Eingebautes Caching, Edge-Deployment, H3-basiert. Kompiliert zu einem deploymentfähigen Bundle für alle Plattformen.
Server Routes
/server/api/ als sicherer API-Layer. Datenbankzugriffe, Auth und sensible Logik hier — nie im Client-Code. Nitro-Caching mit defineCachedEventHandler.
useFetch vs. $fetch
useFetch: SSR-aware, reaktiv, für Page-Render-Daten. $fetch: programmatisch, imperativ, für Button-Clicks und Mutations. Nicht austauschbar.
Hybrid Rendering
routeRules in nuxt.config.ts: SSR, SSG, ISR und SPA pro Route. Marketing-Seiten statisch, Checkout SSR, Admin SPA — alles in einem Nuxt-Projekt.