← Zurück zum Blog

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 ChangeImpactAufwand
Node 16 Support weg🔴 HochMittel
app.config.ts entfernt🟡 MittelGering bis Mittel
SWR nicht mehr default🟡 MittelGering
Async Context ist default🟢 GeringSehr gering
Nitro Plugin API geändert🟡 MittelGering

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

EnvironmentWas checkenWie fixen
Lokalnode -vnvm install 20 oder nvm use 20
DockerFROM node:...FROM node:20-alpine
GitHub Actionsactions/setup-node@v4node-version: '20'
GitLab CIimage: node:...image: node:20-alpine
VercelProject Settings → Node VersionSet to 20.x
Netlify.nvmrc oder Build SettingsCreate .nvmrc with 20
VPS / Servernode -vnvm 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 als maxAge?"

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 laden
  • maxAge: 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:

ChangeImpactFix
useStorage() Key-FormatNiedrigKeys müssen jetzt Prefix haben
Route Rules SyntaxNiedrigLeicht angepasst
Fetch-DefaultsNiedrigDefaults 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:

  1. Node 18+ Pflicht → Überall upgraden (größter Aufwand)
  2. app.config.ts wegruntimeConfig nutzen (besser für Secrets)
  3. SWR nicht mehr default → Explizit machen (weniger Bugs)
  4. Async Context default → Besseres Logging & Tracing
  5. Plugin API geändert → Kleiner Fix

Die Realität:

Wenn du:

  • schon auf Nuxt 4 bist
  • Node 18+ überall hast
  • runtimeConfig nutzt

… 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:

  1. Nuxt 5 kommt
  2. Nitro v3 Tasks & Background Jobs
  3. Nitro v3 WebSockets
  4. Nitro v3 Breaking Changes (dieser Artikel)
  5. 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?