ohne zweiten Server, ohne zusätzliche Infrastruktur
Wer für sein Nuxt Frontend ein separates Node oder PHP Backend aufsetzt, verdoppelt oft unnötig die Infrastruktur. Nuxt Server Routes bauen auf dem Nitro Server auf und liefern Routing, Validierung, Fehlerbehandlung und Caching in derselben Codebasis, deployt als ein einziges Deployment Artefakt.
Inhaltsverzeichnis
- 1. Was Nuxt Server Routes wirklich sind
- 2. Routing Konventionen: Datei, Methode, Parameter
- 3. Request und Response sauber handhaben
- 4. Validierung mit Zod in Server Routes
- 5. Service Layer statt Logik im Handler
- 6. Einheitliche Fehlerformate für die API
- 7. Caching mit Nitro und defineCachedEventHandler
- 8. Testing von Server Routes mit Vitest
- 9. Deployment: Node, Serverless oder Edge im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Nuxt Server Routes wirklich sind
Eine Nuxt Server Route ist eine Datei im Verzeichnis server/api oder server/routes, die von Nitro, dem Server Engine unter Nuxt, direkt in einen HTTP Handler übersetzt wird. Anders als bei einem separaten Express oder Fastify Backend gibt es keine zweite Codebasis, keinen zweiten Build Prozess und keinen zweiten Deployment Schritt. Die Nuxt Server Routes laufen im selben Prozess wie das gerenderte Frontend, teilen sich denselben TypeScript Konfigurationskontext und werden zusammen mit dem Rest der Anwendung gebaut.
Der praktische Nutzen zeigt sich in kleinen und mittleren Projekten besonders deutlich: statt zwei Repositories, zwei CI Pipelines und zwei Hosting Verträge zu pflegen, reicht ein einziges Nuxt Projekt mit Nuxt Server Routes für Authentifizierung, Datenzugriff und Drittanbieter Integrationen. Für größere Systeme mit eigenem Backend Team bleibt ein dediziertes Backend oft sinnvoller, aber für Content Seiten, interne Tools und kleinere SaaS Produkte sind Nuxt Server Routes häufig die pragmatischere Wahl.
Wichtig ist die Abgrenzung zu API Routen in reinen Frontend Frameworks: Nitro ist kein einfacher Proxy, sondern ein vollwertiger Server Runtime, der auf verschiedenen Plattformen läuft, von Node über Deno bis zu Cloudflare Workers. Diese Portabilität ist einer der Hauptgründe, warum Nuxt Server Routes auch für produktionsreife APIs infrage kommen, nicht nur für kleine Mock Endpunkte während der Entwicklung.
2. Routing Konventionen: Datei, Methode, Parameter
Die Routing Konvention von Nuxt Server Routes orientiert sich am Dateinamen. Eine Datei server/api/products.get.ts reagiert nur auf GET Anfragen an /api/products, eine Datei products.post.ts im selben Verzeichnis nur auf POST. Dieses Suffix Muster verhindert, dass ein Handler versehentlich für die falsche HTTP Methode aufgerufen wird, und macht auf einen Blick sichtbar, welche Methoden ein Endpunkt unterstützt, ohne den Dateiinhalt öffnen zu müssen.
Dynamische Segmente werden mit eckigen Klammern markiert: server/api/products/[id].get.ts liefert die ID über getRouterParam(event, 'id'). Catch all Routen mit [...slug].ts fangen beliebig tiefe Pfade ab, praktisch für Proxy Endpunkte oder generische Content APIs. Verschachtelte Verzeichnisse spiegeln verschachtelte Ressourcen, sodass server/api/orders/[orderId]/items.get.ts exakt der REST Konvention für untergeordnete Ressourcen entspricht.
// server/api/products/[id].get.ts
// GET /api/products/:id — single product lookup
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
if (!id) {
throw createError({ statusCode: 400, statusMessage: 'Missing product id' })
}
const product = await findProductById(id)
if (!product) {
throw createError({ statusCode: 404, statusMessage: 'Product not found' })
}
return product
})
// server/api/products/index.post.ts
// POST /api/products — create a new product
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const created = await createProduct(body)
setResponseStatus(event, 201)
return created
})
Ein häufiger Anfängerfehler bei Nuxt Server Routes: die Reihenfolge der Auswertung bei überlappenden Routen wird falsch eingeschätzt. Nitro bevorzugt statische Segmente vor dynamischen, sodass products/featured.get.ts vor products/[id].get.ts greift, selbst wenn beide Dateien im selben Verzeichnis liegen. Wer das nicht kennt, wundert sich, warum eine vermeintlich spezifische Route nie erreicht wird.
3. Request und Response sauber handhaben
Jede Nuxt Server Route erhält ein H3Event Objekt, über das sämtliche Request Informationen zugänglich sind. getQuery(event) liefert Query Parameter als Objekt, readBody(event) parst den Body abhängig vom Content Type automatisch als JSON, getHeader(event, 'authorization') liest einzelne Header aus. Diese Helper Funktionen sind global verfügbar, ohne Import, dank Nitros Auto Import Mechanismus, was den Code merklich kompakter hält als vergleichbare Express Middleware Ketten.
Für die Antwortseite bestimmt setResponseStatus(event, code) den HTTP Status, setHeader(event, name, value) setzt Response Header, und der Rückgabewert der Handler Funktion wird automatisch serialisiert. Gibt eine Nuxt Server Route ein Objekt zurück, wird es als JSON gesendet, gibt sie einen String zurück, als Text. Für Streaming Antworten steht sendStream(event, stream) zur Verfügung, relevant etwa bei Datei Downloads oder Server Sent Events.
// server/api/search.get.ts
// GET /api/search?q=vue&page=2&limit=20
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const searchTerm = String(query.q ?? '')
const page = Number(query.page ?? 1)
const limit = Math.min(Number(query.limit ?? 20), 100)
setHeader(event, 'X-Total-Count', String(await countResults(searchTerm)))
setResponseStatus(event, 200)
return {
results: await searchProducts(searchTerm, page, limit),
page,
limit,
}
})
4. Validierung mit Zod in Server Routes
Ohne explizite Validierung akzeptiert eine Nuxt Server Route jeden Body, den ein Client schickt, inklusive fehlender Felder, falscher Typen und potenziell schädlicher Werte. Zod hat sich als Standard Bibliothek für Validierung im Nuxt Ökosystem etabliert, weil sie TypeScript Typen direkt aus dem Schema ableitet, statt Typen und Validierungsregeln getrennt zu pflegen. Ein Schema definiert gleichzeitig die Laufzeitprüfung und die statische Typinformation für den Rest des Handlers.
Die Kombination aus readValidatedBody und einem Zod Schema ersetzt manuelle if Ketten vollständig. Schlägt die Validierung fehl, wirft die Funktion automatisch einen 400er Fehler mit einer strukturierten Fehlermeldung, die alle fehlgeschlagenen Felder auflistet. Für Nuxt Server Routes mit komplexeren Eingaben, etwa verschachtelten Bestellobjekten, lohnt sich ein zentrales Schema Verzeichnis, aus dem sowohl Server Routes als auch Formulare im Frontend importieren.
// server/utils/schemas.ts
import { z } from 'zod'
export const createProductSchema = z.object({
name: z.string().min(2).max(200),
price: z.number().positive(),
sku: z.string().regex(/^[A-Z0-9-]+$/),
categoryIds: z.array(z.number()).min(1),
})
// server/api/products/index.post.ts
export default defineEventHandler(async (event) => {
// Throws a structured 400 error automatically on invalid input
const data = await readValidatedBody(event, createProductSchema.parse)
const created = await createProduct(data)
setResponseStatus(event, 201)
return created
})
5. Service Layer statt Logik im Handler
Ein Muster, das in wachsenden Projekten mit Nuxt Server Routes regelmäßig zu Problemen führt, ist Geschäftslogik direkt im Event Handler. Zu Beginn wirkt das praktisch, weil ein Handler wenige Zeilen umfasst, doch sobald mehrere Routen dieselbe Berechnung, Validierung oder Datenbankabfrage brauchen, entsteht Code Duplikation über mehrere Dateien hinweg. Der Ausweg ist eine dünne Service Schicht in server/utils, die reine Funktionen ohne Abhängigkeit vom H3Event exportiert.
Diese Trennung macht Services unabhängig testbar, ohne einen echten HTTP Request zu simulieren, und erlaubt, dieselbe Logik von mehreren Nuxt Server Routes, geplanten Tasks oder sogar CLI Skripten aus aufzurufen. Der Handler bleibt dünn: Request lesen, validieren, Service aufrufen, Antwort formen. Diese vier Schritte sollten in nahezu jeder Server Route erkennbar sein, was Code Reviews erheblich beschleunigt.
// server/utils/productService.ts
// Pure business logic, no H3Event dependency, easy to unit test
export async function createProduct(input: CreateProductInput) {
const existing = await db.product.findFirst({ where: { sku: input.sku } })
if (existing) {
throw createError({ statusCode: 409, statusMessage: 'SKU already exists' })
}
return db.product.create({ data: input })
}
// server/api/products/index.post.ts
export default defineEventHandler(async (event) => {
const data = await readValidatedBody(event, createProductSchema.parse)
const created = await createProduct(data) // service handles the logic
setResponseStatus(event, 201)
return created
})
6. Einheitliche Fehlerformate für die API
Ein Client, der gegen eine API aus Nuxt Server Routes arbeitet, sollte sich auf ein konsistentes Fehlerformat verlassen können, unabhängig davon, welche Route den Fehler auslöst. createError erzeugt einen H3 Error mit statusCode, statusMessage und optional einem data Feld für zusätzliche Details wie Validierungsfehler pro Feld. Wird dieser Fehler geworfen, formatiert Nitro automatisch eine passende JSON Antwort mit demselben Status Code.
Für unterschiedliche Fehlerklassen empfiehlt sich eine kleine Fehlerhierarchie: ValidationError für 400er, NotFoundError für 404er, ConflictError für 409er. Diese Klassen wrappen createError und geben jeder Nuxt Server Route ein konsistentes Vokabular, statt an jeder Stelle den Status Code manuell zu tippen. Ein globaler Error Handler in server/plugins/errorHandler.ts kann zusätzlich unerwartete Fehler loggen, bevor sie den Client erreichen, ohne interne Details preiszugeben.
7. Caching mit Nitro und defineCachedEventHandler
Nitro bringt eingebautes Caching für Nuxt Server Routes mit, ohne dass eine externe Redis Instanz zwingend notwendig ist. defineCachedEventHandler wrappt einen bestehenden Handler und cached dessen Antwort basierend auf konfigurierbaren Schlüsseln, etwa Query Parametern oder Route Segmenten. Für Endpunkte mit teuren Datenbankabfragen oder externen API Aufrufen reduziert das die Latenz drastisch, ohne die Handler Logik zu verändern.
Die Konfiguration erfolgt über maxAge für die Time to Live und getKey für die Cache Schlüssel Generierung. In Produktion lässt sich der Cache Storage Treiber austauschen, von einfachem Filesystem Cache bis zu Redis oder Cloudflare KV, ohne den Handler Code anzufassen. Wichtig für Nuxt Server Routes mit personalisierten Antworten: der Cache Schlüssel muss die Nutzeridentität einbeziehen, sonst landen Daten eines Nutzers im Cache eines anderen.
// server/api/categories.get.ts
// Cached for 5 minutes, keyed by query string
export default defineCachedEventHandler(
async (event) => {
return await db.category.findMany({ orderBy: { name: 'asc' } })
},
{
maxAge: 60 * 5,
getKey: (event) => `categories:${getQuery(event).lang ?? 'de'}`,
}
)
8. Testing von Server Routes mit Vitest
Nuxt Server Routes lassen sich auf zwei Ebenen testen: isolierte Unit Tests für die Service Funktionen und integrative Tests, die den kompletten Handler über einen echten HTTP Aufruf prüfen. Für Unit Tests reicht Vitest ohne besonderes Setup, da Services aus server/utils reine Funktionen sind. Für Integrationstests bietet @nuxt/test-utils die Funktion $fetch, die gegen einen laufenden Test Server aufruft und damit Routing, Validierung und Serialisierung gemeinsam prüft.
Der Vorteil von Integrationstests für Nuxt Server Routes liegt darin, dass Regressionen im Routing selbst auffallen, etwa eine falsch benannte Datei oder ein vergessener HTTP Methodensuffix, die reine Unit Tests der Services nicht erfassen würden. In der Praxis bewährt sich eine Mischung: viele schnelle Unit Tests für Geschäftslogik, wenige, aber aussagekräftige Integrationstests für die wichtigsten Endpunkte.
// tests/api/products.test.ts
import { describe, it, expect } from 'vitest'
import { setup, $fetch } from '@nuxt/test-utils/e2e'
describe('products API', async () => {
await setup({ server: true })
it('returns 404 for unknown product id', async () => {
await expect($fetch('/api/products/does-not-exist')).rejects.toThrow('404')
})
it('creates a product with valid payload', async () => {
const result = await $fetch('/api/products', {
method: 'POST',
body: { name: 'Test Product', price: 19.99, sku: 'TEST-001', categoryIds: [1] },
})
expect(result.sku).toBe('TEST-001')
})
})
9. Deployment: Node, Serverless oder Edge im Vergleich
Weil Nitro mehrere Deployment Ziele unterstützt, muss man sich bei Nuxt Server Routes früh entscheiden, auf welcher Plattform die API laufen soll. Die Wahl beeinflusst, welche Node APIs verfügbar sind, wie lange ein Request maximal laufen darf, und wie Cold Starts sich auf die Latenz auswirken. Ein klassisches Node Deployment auf einem VPS oder in einem Container bietet die größte Kompatibilität und keine Zeitlimits, verlangt aber eigenes Server Management.
Serverless Plattformen wie Vercel oder Netlify Functions übernehmen Skalierung automatisch, führen aber zu Cold Starts bei seltenem Traffic und begrenzen die maximale Ausführungszeit pro Request. Edge Deployment, etwa auf Cloudflare Workers, bietet die niedrigste Latenz durch globale Verteilung, schränkt aber die verfügbare Node API stark ein, sodass manche Datenbank Treiber oder native Module nicht funktionieren. Für Nuxt Server Routes mit klassischen SQL Datenbanktreibern ist Node Deployment meist die sicherste Wahl.
| Ziel | Cold Starts | Node Kompatibilität | Betrieb |
|---|---|---|---|
| Node Server | Keine | Vollständig | Eigenes Server Management nötig |
| Serverless (Vercel) | Gelegentlich | Fast vollständig | Automatische Skalierung |
| Edge (Cloudflare Workers) | Minimal | Eingeschränkt | Globale Verteilung, wenig Node APIs |
| Container (Docker) | Keine | Vollständig | Volle Kontrolle, mehr Ops Aufwand |
Der Preset Mechanismus von Nitro macht den Wechsel zwischen diesen Zielen erfreulich unkompliziert: nitro.preset in der nuxt.config.ts steuert, für welche Plattform gebaut wird, ohne dass der Handler Code für Nuxt Server Routes angepasst werden muss. In der Praxis lohnt es sich, früh im Projekt einen Presets Test durchzuführen, um Inkompatibilitäten mit Datenbanktreibern oder nativen Abhängigkeiten frühzeitig zu erkennen, statt sie erst beim ersten Produktionsdeployment zu entdecken.
Mironsoft
Vue.js und Nuxt Entwicklung für produktive Frontends und Backends
Ein Backend, das in Nuxt schon eingebaut ist?
Wir bauen Nuxt Server Routes, die produktionsreif sind: mit Validierung, Fehlerbehandlung, Caching und dem passenden Deployment Ziel für euren Use Case.
API Design
Routing, Validierung und Fehlerformate für Nuxt Server Routes
Performance
Caching Strategien mit Nitro für schnelle Antwortzeiten
Deployment
Das passende Nitro Preset für euer Hosting auswählen
10. Zusammenfassung
Nuxt Server Routes liefern ein vollwertiges Backend innerhalb derselben Codebasis wie das Frontend: Datei basiertes Routing über server/api, klare HTTP Methodensuffixe, globale Helper Funktionen wie readBody und getQuery, sowie Zugriff auf das komplette H3 Event für Header und Status Codes. Validierung mit Zod verhindert fehlerhafte Eingaben strukturiert, ein dünner Service Layer hält Handler lesbar und wiederverwendbar, und einheitliche Fehlerformate machen die API für Clients vorhersehbar.
Caching über defineCachedEventHandler reduziert Latenz ohne externen Cache Server, Vitest Integrationstests über $fetch decken Routing Regressionen zuverlässig auf, und die Wahl des Nitro Presets entscheidet über Node Kompatibilität, Cold Starts und Betriebsaufwand. Wer diese Bausteine konsequent nutzt, baut mit Nuxt Server Routes APIs, die auch unter Produktionslast bestehen, ohne ein separates Backend Projekt zu benötigen.
Nuxt Server Routes als Backend — Das Wichtigste auf einen Blick
Routing
Dateibasiert in server/api, HTTP Methode über Dateisuffix, dynamische Segmente mit eckigen Klammern.
Validierung
Zod Schemas mit readValidatedBody, ein Schema für Laufzeitprüfung und TypeScript Typ zugleich.
Struktur
Service Layer in server/utils ohne H3Event Abhängigkeit, Handler bleibt dünn und testbar.
Betrieb
Caching mit defineCachedEventHandler, Nitro Preset passend zur Zielplattform wählen.