Nitro v3 Breaking Changes - was wirklich weh tut (und wie du dich vorbereitest)
Ein typischer Moment bei jedem Framework-Upgrade:
„Was bricht alles?"
Die ehrliche Antwort bei Nitro v3:
„Einiges. Aber das Meiste macht dein Projekt langfristig besser."
Das klingt erstmal unbefriedigend.
Aber es ist wichtig zu verstehen:
Breaking Changes sind nicht „einfach so da" - sie beheben echte Probleme.
Mit Nitro v3 kommen strukturelle Änderungen, die Verhalten:
- klarer
- vorhersehbarer
- wartbarer
machen.
In diesem Artikel schauen wir uns an:
- Welche Breaking Changes wirklich Impact haben
- Warum sie eingebaut wurden (und warum das oft gut ist)
- Wie du dein Projekt vorbereitest
- Worauf du bei der Migration achten musst
Die 5 wichtigsten Breaking Changes
Nitro v3 bringt mehrere Breaking Changes mit.
Aber nicht alle sind gleich relevant.
Hier sind die 5 wichtigsten, sortiert nach Impact:
| Breaking Change | Impact | Aufwand |
|---|---|---|
| Node 16 Support weg | 🔴 Hoch | Mittel |
| app.config.ts entfernt | 🟡 Mittel | Gering bis Mittel |
| SWR nicht mehr default | 🟡 Mittel | Gering |
| Async Context ist default | 🟢 Gering | Sehr gering |
| Nitro Plugin API geändert | 🟡 Mittel | Gering |
Jetzt im Detail.
1) Node 16 Support ist weg → Node 18+ Pflicht
Das ist der Breaking Change mit dem größten Impact.
Was sich ändert
Nitro v3 droppt Support für:
- Node 14 (war eh schon weg in Nuxt 3)
- Node 16 (ab jetzt nicht mehr unterstützt)
Ab Nitro v3 brauchst du:
- Node 18 (LTS)
- oder Node 20 (aktuelles LTS)
- oder Node 21+ (current)
Warum das ein Problem ist
Wenn du denkst:
„Ist doch nur Node upgraden, was soll's?"
… dann unterschätzt du den Impact.
Weil Node nicht nur lokal laufen muss.
Sondern auch:
- Docker Images (Base Image muss passen)
- CI/CD Pipelines (GitHub Actions, GitLab CI, etc.)
- Hosting (Vercel, Netlify, VPS, etc.)
- Development Environment (alle Entwickler im Team)
- Staging / QA Environments
Das ist nicht „einfach Node upgraden".
Das ist:
„Überall Node upgraden."
Checkliste: Wo du Node 18+ sicherstellen musst
| Environment | Was checken | Wie fixen |
|---|---|---|
| Lokal | node -v | nvm install 20 oder nvm use 20 |
| Docker | FROM node:... | FROM node:20-alpine |
| GitHub Actions | actions/setup-node@v4 | node-version: '20' |
| GitLab CI | image: node:... | image: node:20-alpine |
| Vercel | Project Settings → Node Version | Set to 20.x |
| Netlify | .nvmrc oder Build Settings | Create .nvmrc with 20 |
| VPS / Server | node -v | nvm install 20 oder Package Manager |
Docker Beispiel
Alt (Node 16):
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", ".output/server/index.mjs"]
Neu (Node 20):
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", ".output/server/index.mjs"]
GitHub Actions Beispiel
Alt:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "16"
- run: npm ci
- run: npm run build
Neu:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20" # ← changed
- run: npm ci
- run: npm run build
Warum Node 18+ Pflicht ist
Das ist keine willkürliche Entscheidung.
Node 18 bringt wichtige Features, die Nitro v3 nutzt:
- fetch API (nativ, ohne Polyfill)
- WebStreams API
- Web Crypto API
- Test Runner (native testing)
- bessere Performance
Außerdem:
- Node 16 ist End-of-Life (seit April 2024)
- keine Security Updates mehr
- aktiv auf alter Version zu bleiben ist ein Security Risk
Was du jetzt machen solltest
Schritt 1: Lokale Version checken
node -v
Wenn < 18: upgraden.
Schritt 2: Projekt testen mit Node 18/20
nvm install 20
nvm use 20
npm ci
npm run dev
Wenn alles läuft: gut.
Wenn nicht: Dependencies checken (manche alte Packages funktionieren nicht mit Node 20).
Schritt 3: CI/CD anpassen
Alle Pipelines auf Node 18+ umstellen.
Schritt 4: Deployment anpassen
Docker, Vercel, Netlify, VPS - überall checken.
Schritt 5: Team informieren
Alle Entwickler müssen lokal auf Node 18+ upgraden.
2) app.config.ts wird entfernt
Das ist ein konzeptioneller Breaking Change.
Was sich ändert
In Nitro v2 konntest du app.config.ts nutzen für:
- globale Config
- Runtime Config
- App-spezifische Settings
In Nitro v3 ist app.config.ts weg.
Warum das eigentlich gut ist
Auf den ersten Blick klingt das nervig.
In der Praxis ist es aber eine sehr gute Breaking Change.
Warum?
Weil app.config.ts in echten Projekten oft zu einem „magischen globalen Ding" wurde:
- niemand wusste mehr, was runtime ist
- was build-time ist
- was public vs private ist
- und wie man Secrets sauber trennt
Beispiel für Chaos:
// app.config.ts (alt)
export default defineAppConfig({
apiUrl: "https://api.example.com", // public? private?
apiKey: process.env.API_KEY, // ⚠️ Secrets im Frontend?
theme: "dark", // OK
maxUploadSize: 10485760, // Build-time oder Runtime?
});
Das führte oft zu:
- Secrets im Frontend-Bundle (💀)
- Config-Chaos
- Schwer zu debuggen
Migration: Von app.config.ts zu runtimeConfig
Alt (app.config.ts):
// app.config.ts
export default defineAppConfig({
apiUrl: "https://api.example.com",
apiKey: process.env.API_KEY,
theme: "dark",
});
Neu (nuxt.config.ts):
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// Private (nur server-side)
apiKey: process.env.API_KEY,
// Public (client + server)
public: {
apiUrl: "https://api.example.com",
theme: "dark",
},
},
});
Wichtig:
Alles unter runtimeConfig.public ist im Frontend sichtbar.
Alles direkt unter runtimeConfig ist nur server-side.
Zugriff auf Runtime Config
Server-Side:
// server/api/data.ts
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig();
// Private config (nur server-side)
const apiKey = config.apiKey;
// Public config
const apiUrl = config.public.apiUrl;
return { apiUrl }; // apiKey NIE returnen!
});
Client-Side:
<script setup>
const config = useRuntimeConfig();
// Nur public config verfügbar
console.log(config.public.apiUrl); // ✅
console.log(config.apiKey); // ❌ undefined (nicht verfügbar)
</script>
Warum das besser ist
Mit runtimeConfig ist jetzt klar:
- Private Secrets =
runtimeConfig.apiKey - Public Config =
runtimeConfig.public.apiUrl
Keine Grauzone mehr.
Keine Secrets im Frontend.
3) SWR ist nicht mehr default
Das ist ein Verhalten-Breaking-Change, der subtil ist.
Was sich ändert
In Nitro v2 war Stale-While-Revalidate (SWR) bei cached Functions implizit aktiv.
Das bedeutete:
- Cached Response wird sofort zurückgegeben
- Im Hintergrund wird neu geladen
- Nächster Request bekommt frische Daten
In Nitro v3 ist SWR nicht mehr default.
Du musst es explizit aktivieren.
Warum das oft zu Bugs führte
Das implizite SWR-Verhalten in Nitro v2 führte zu sehr typischen Problemen:
Szenario:
// server/api/data.ts
export default defineCachedEventHandler(
async (event) => {
return await fetchExpensiveData();
},
{
maxAge: 60, // 60 Sekunden Cache
},
);
Was Entwickler erwarten:
- 60 Sekunden Cache
- danach frische Daten
Was wirklich passierte:
- 60 Sekunden Cache
- dann SWR (alte Daten + Background Reload)
- effektiv: Cache war deutlich länger aktiv
Das führte zu:
„Warum zeigt die API alte Daten?"
„Warum ist das Verhalten manchmal anders?"
„Warum ist der Cache scheinbar länger aktiv alsmaxAge?"
Migration: SWR explizit machen
Neu in Nitro v3:
// server/api/data.ts
export default defineCachedEventHandler(
async (event) => {
return await fetchExpensiveData();
},
{
maxAge: 60,
swr: true, // ← explizit aktivieren
},
);
Wenn du SWR NICHT brauchst:
export default defineCachedEventHandler(
async (event) => {
return await fetchExpensiveData();
},
{
maxAge: 60,
// swr: false ist default
},
);
Warum das besser ist
Mit explizitem SWR ist jetzt klar:
maxAge: 60= 60 Sekunden Cache, dann neu ladenmaxAge: 60, swr: true= 60 Sekunden Cache, dann SWR
Kein „magisches Verhalten" mehr.
Weniger überraschende Bugs.
4) Async Context ist default (AsyncLocalStorage)
Das ist ein positiver Breaking Change.
Was sich ändert
Nitro v3 aktiviert Async Context standardmäßig.
Das bedeutet:
- Request-spezifische Daten können durch async Operations „durchgereicht" werden
- Logging, Tracing, Request IDs funktionieren sauber
In Nitro v2 war das:
- opt-in via Flag
- musste aktiviert werden
In Nitro v3 ist das:
- default
Warum das wichtig ist
Async Context ist extrem relevant für:
- Request-scoped Logging (jeder Log-Eintrag weiß, zu welchem Request er gehört)
- Correlation IDs (Request-ID durch alle async Calls hindurch)
- Tracing (Performance Monitoring über Request hinweg)
- Debugging in SaaS (welcher User hat welchen Request ausgelöst?)
Beispiel (ohne Async Context):
// server/api/data.ts
export default defineEventHandler(async (event) => {
console.log("Fetching data..."); // ⚠️ Welcher Request?
const data = await fetchData();
console.log("Data fetched"); // ⚠️ Welcher Request?
return data;
});
Wenn mehrere Requests parallel laufen:
Fetching data...
Fetching data...
Data fetched
Fetching data...
Data fetched
Data fetched
Welcher Log gehört zu welchem Request? Keine Ahnung.
Beispiel (mit Async Context):
// server/middleware/request-id.ts
export default defineEventHandler((event) => {
event.context.requestId = generateRequestId();
});
// server/utils/logger.ts
export function log(message: string) {
const event = useEvent();
const requestId = event?.context?.requestId || "unknown";
console.log(`[${requestId}] ${message}`);
}
// server/api/data.ts
export default defineEventHandler(async (event) => {
log("Fetching data..."); // ✅ [abc123] Fetching data...
const data = await fetchData();
log("Data fetched"); // ✅ [abc123] Data fetched
return data;
});
Jetzt:
[abc123] Fetching data...
[def456] Fetching data...
[abc123] Data fetched
[ghi789] Fetching data...
[def456] Data fetched
[ghi789] Data fetched
Klar zuordenbar.
Was du beachten musst
Async Context ist meistens positiv.
Aber:
In sehr seltenen Fällen kann es zu Performance-Overhead führen (minimal).
Wenn du das wirklich brauchst, kannst du es deaktivieren:
// nitro.config.ts
export default defineNitroConfig({
experimental: {
asyncContext: false,
},
});
Aber:
Das solltest du nur machen, wenn du wirklich einen Grund hast.
In 99% der Projekte ist Async Context ein Gewinn.
5) Nitro Plugin API geändert
Das betrifft dich nur, wenn du Nitro Plugins geschrieben hast.
Was sich ändert
Das Plugin API wurde leicht angepasst:
Alt (Nitro v2):
// server/plugins/my-plugin.ts
export default defineNitroPlugin((nitro) => {
console.log("Plugin loaded");
});
Neu (Nitro v3):
// server/plugins/my-plugin.ts
export default defineNitroPlugin((nitroApp) => {
console.log("Plugin loaded");
});
Der Parameter heißt jetzt nitroApp statt nitro.
Migration
Einfach den Parameter-Namen ändern:
// Alt
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook("request", () => {
console.log("Request");
});
});
// Neu
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook("request", () => {
console.log("Request");
});
});
Das war's.
Weitere kleinere Breaking Changes
Es gibt noch ein paar kleinere Änderungen:
| Change | Impact | Fix |
|---|---|---|
useStorage() Key-Format | Niedrig | Keys müssen jetzt Prefix haben |
| Route Rules Syntax | Niedrig | Leicht angepasst |
| Fetch-Defaults | Niedrig | Defaults für $fetch geändert |
Diese sind in den meisten Projekten nicht relevant.
Falls doch: Die offizielle Nitro v3 Migration Guide hat Details.
Checkliste: Dein Projekt auf Nitro v3 vorbereiten
Hier ist eine pragmatische Checkliste:
Phase 1: Vorbereitung (bevor du upgradest)
- Node 18+ überall sicherstellen (lokal, CI/CD, Docker, Hosting)
- Dependencies checken (sind alle mit Node 18+ kompatibel?)
- app.config.ts migrieren (zu
runtimeConfig) - Cached Functions checken (SWR explizit machen, falls gewünscht)
- Nitro Plugins checken (Parameter-Name anpassen)
Phase 2: Migration (Upgrade durchführen)
- Nuxt 5 installieren (
npm install nuxt@latest) - Build testen (
npm run build) - Dev Server testen (
npm run dev) - Tests laufen lassen (falls vorhanden)
Phase 3: Testing (nach Upgrade)
- Functionality testen (alle wichtigen Flows durchgehen)
- Caching Verhalten checken (ist SWR wie erwartet?)
- Logging testen (funktioniert Async Context?)
- Performance checken (ist alles noch schnell?)
Phase 4: Deployment
- Staging Deployment (erstmal nicht Production!)
- Smoke Tests (kritische Features prüfen)
- Monitoring (Fehlerrate, Performance)
- Production Deployment (gestaffelt, wenn möglich)
Testing: Wie du Probleme früh erkennst
Der beste Weg, um Probleme zu finden:
Nightly Builds testen.
Wenn du ein größeres Projekt hast:
# Install Nuxt 5 Nightly
npm install nuxt@nightly
# Try build
npm run build
# Try dev
npm run dev
Das zeigt dir:
- was bricht
- wie viel Aufwand die Migration ist
- wo die Risiken liegen
Wichtig:
Das ist nicht für Production.
Das ist nur, um früh zu sehen: „Was kommt auf uns zu?"
Fazit
Nitro v3 bringt Breaking Changes - keine Frage.
Aber:
Die meisten machen dein Projekt langfristig besser.
Die wichtigsten Changes:
- Node 18+ Pflicht → Überall upgraden (größter Aufwand)
- app.config.ts weg →
runtimeConfignutzen (besser für Secrets) - SWR nicht mehr default → Explizit machen (weniger Bugs)
- Async Context default → Besseres Logging & Tracing
- Plugin API geändert → Kleiner Fix
Die Realität:
Wenn du:
- schon auf Nuxt 4 bist
- Node 18+ überall hast
runtimeConfignutzt
… dann ist der Upgrade zu Nuxt 5 überschaubar.
Aber:
Plan das nicht als „schnelles npm update".
Plan es als:
- Runtime Audit
- Migration Tests
- Staged Rollout
Dann wird es gut.
Die Artikel-Serie:
- Nuxt 5 kommt
- Nitro v3 Tasks & Background Jobs
- Nitro v3 WebSockets
- Nitro v3 Breaking Changes (dieser Artikel)
- Migration Guide: Nuxt 5
Breaking Changes sind nicht „schlecht" - sie sind oft der Preis für bessere Architektur. Die Frage ist nur: Bist du vorbereitet?