Next.js Parallel Routes und Interception Routes: Praktische Patterns für komplexe UI

#Next.js Parallel Routes
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Next.js Parallel Routes und Interception Routes sind die beiden App-Router-Features, die Teams am häufigsten überspringen, und gleichzeitig die beiden, die ihnen am meisten Client-State ersparen würden. Beide sind seit Next.js 13.4 stabil und beide sind dokumentiert. Trotzdem werden sie ignoriert, weil die Ordnerkonventionen beim ersten Kontakt seltsam aussehen. Ordner mit Namen wie @metrics oder (.)invoices wirken wie Zeilenrauschen, bis das Modell dahinter klickt.

Das Modell lohnt sich. In einem typischen B2B-SaaS-Dashboard ersetzen diese beiden Features überraschend viel Code: den Boolean, der festhält, ob ein Drawer offen ist, und den Context Provider, der den ausgewählten Datensatz herumträgt. Dazu der Effect, der alles zurück in die URL synchronisiert, damit ein Refresh den Nutzer nicht stranden lässt, und du hast die Hälfte der Dashboard-Codebasen beschrieben, die wir auditieren. Der Router kann diese Arbeit bereits. Du musst sie ihm nur übergeben.

Was folgt, sind die Patterns, die wir in Kundenprojekten einsetzen, Ordnerstrukturen inklusive.

Was Next.js Parallel Routes tatsächlich tun

Eine Parallel Route erlaubt einem Layout, mehrere Pages gleichzeitig zu rendern. Du definierst Slots, indem du einen Ordner mit @ prefixst, und jeder Slot kommt als Prop im darüberliegenden Layout an.

app/
  dashboard/
    layout.tsx
    page.tsx          // der implizite "children"-Slot
    @metrics/
      page.tsx
      loading.tsx
      error.tsx
    @activity/
      page.tsx
      loading.tsx

Das Layout entscheidet, wo welche Region landet:

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  metrics,
  activity,
}: {
  children: React.ReactNode
  metrics: React.ReactNode
  activity: React.ReactNode
}) {
  return (
    <div className="grid grid-cols-3 gap-6">
      <main className="col-span-2">{children}</main>
      <div className="space-y-6">
        {metrics}
        {activity}
      </div>
    </div>
  )
}

Zwei Details sind hier wichtig. Slots sind keine URL-Segmente: /dashboard rendert alle drei Regionen, und es gibt keine Adresse /dashboard/@metrics. Und jede Slot-Page ist eine vollständige Server Component, die ihre eigenen Daten nach ihren eigenen Caching-Regeln lädt. Wenn die Metrics-Abfrage 800 ms dauert und der Activity-Feed 80 ms, streamt der schnelle zuerst rein. Caching pro Fetch funktioniert wie überall im App Router; die Regeln haben wir in unserem Guide zu Next.js 15 Caching durchgespielt.

Du könntest dieselbe Seite als eine Komponente mit zwei Suspense Boundaries und zwei datenladenden Kindern bauen. Das funktioniert. Aber die Parallel-Route-Variante gibt jeder Region per Konvention eigene Loading- und Error-Dateien, und genau da beginnt sich das Pattern auszuzahlen.

Unabhängige Loading- und Error-Boundaries pro Slot

Jeder Slot bekommt seine eigene loading.tsx und error.tsx, begrenzt auf diesen Slot allein. Wenn der Activity-Service ausfällt, rendert @activity/error.tsx in dieser Grid-Zelle und der Rest des Dashboards bleibt bedienbar. Der Nutzer behält seine Metriken. Niemand starrt auf einen ganzseitigen Fehlerbildschirm, weil ein nachgelagerter Service ein Timeout hatte.

Diese Isolation ist das stärkste Argument für Parallel Routes in Dashboard-UI. Monitoring-Seiten und Admin-Übersichten sind genau die Screens, in denen vier Backend-Calls mit vier verschiedenen Latenzen und Fehlerbildern daherkommen. Dieselbe Isolation von Hand zu verdrahten bedeutet eine ErrorBoundary und einen Suspense-Wrapper pro Region, plus die Disziplin, das über jede Dashboard-Seite der App konsistent durchzuhalten. Die Dateikonvention macht Isolation zum Standard statt zu einem Code-Review-Kommentar.

Ein Vorbehalt: error.tsx muss wie überall im App Router eine Client Component sein, und eine Slot-Boundary fängt keine Fehler, die im gemeinsamen Layout selbst geworfen werden.

Die default.tsx-Falle, die 404s verursacht

Das ist der Teil, an dem Teams das Feature aufgeben, deshalb verdient er Präzision.

Slots können eigene Subrouten definieren. Füge @activity/archive/page.tsx plus einen Link auf /dashboard/archive hinzu, und beim Klick wechselt die Activity-Region zu ihrer Archivansicht, während alles andere bleibt, wo es ist. Die URL ändert sich, die Metrics-Region behält ihren aktuellen Inhalt, und das Ganze fühlt sich an wie Tab-State ohne jeglichen Tab-State.

Die Falle ist die harte Navigation. Wenn jemand /dashboard/archive neu lädt oder aus einem Lesezeichen öffnet, muss Next.js jeden Slot von null rendern, und @metrics hat keine Route, die auf /archive passt. Der Router sucht @metrics/default.tsx als Fallback. Existiert diese Datei nicht, liefert die gesamte Seite einen 404 statt nur der betroffene Slot.

Die Regel, die wir durchsetzen: Sobald irgendein Slot eine Subroute definiert, bekommt jeder Geschwister-Slot eine default.tsx. Für eine Region, die weiter ihren normalen Inhalt zeigen soll, re-exportierst du die Page. Für einen Overlay-Slot gibst du null zurück:

// app/dashboard/@metrics/default.tsx
export { default } from './page'
// app/@modal/default.tsx
export default function Default() {
  return null
}

Wenn du eine Sache aus diesem Beitrag mitnimmst, dann diese. Fast jede Beschwerde der Art "Parallel Routes sind kaputt", die wir gesehen haben, ließ sich auf eine fehlende default.tsx zurückführen.

Interception Routes: Ein Modal mit echter URL

Interception Routes lösen ein anderes Problem: den Record-Drawer. Jedes B2B-Produkt hat einen. Klick auf eine Rechnung in der Tabelle und ein Panel schiebt sich mit den Details über die Liste. Product will, dass es sich sofort anfühlt, Support will eine teilbare URL, damit ein Kunde "schau dir diese Rechnung an" an einen Kollegen schicken kann, und ein Direktaufruf dieser URL aus einem E-Mail-Link soll eine richtige Vollseite rendern, weil darunter keine Liste liegt, über die man etwas schieben könnte.

Client-State-Modals erfüllen die erste Anforderung und scheitern an den anderen beiden. Interception Routes erfüllen alle drei aus der Routing-Schicht heraus. Ein Ordner mit einem Marker wie (.) fängt die Navigation zu einer anderen Route ab und rendert stattdessen seinen eigenen Inhalt, während die Adressleiste die Ziel-URL zeigt.

app/
  layout.tsx
  invoices/
    page.tsx            // die Tabelle
    [invoiceId]/
      page.tsx          // vollständige Detailseite
  @modal/
    default.tsx         // gibt null zurück
    (.)invoices/
      [invoiceId]/
        page.tsx        // der Drawer

Das Root-Layout mountet beides:

// app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {children}
        {modal}
      </body>
    </html>
  )
}

Ab hier teilt sich das Verhalten nach Navigationstyp. Eine clientseitige Navigation von der Tabelle zu /invoices/inv_2041 wird abgefangen: Der Drawer rendert im @modal-Slot über der weiterhin gemounteten Tabelle. Eine harte Navigation zur selben URL überspringt die Interception und rendert invoices/[invoiceId]/page.tsx als normale Seite. Du bekommst die teilbare URL und den sauberen Fallback, ohne eine Zeile Modal-State zu schreiben. Der Drawer schließt sich per router.back(), was die Listen-URL wiederherstellt und den Slot über die default.tsx mit null leert.

Die Marker zählen Routen-Segmentebenen, nicht Ordnerverschachtelung, und das ist die zweite Stolperfalle. (.) fängt eine Route auf derselben Ebene ab, (..) eine Ebene höher, (...) ab der App-Root. Slot-Ordner wie @modal und Route Groups in Klammern zählen nicht als Ebenen, deshalb passt (.)invoices in der Struktur oben auf die Top-Level-Routen unter /invoices. Liegt ein Drawer tiefer im Baum, zähle Segmente, nicht Verzeichnisse.

Ein praktischer Hinweis zu Mutationen: Wenn der Drawer ein Formular enthält, schicke es über eine Server Action ab und revalidiere die Listenroute vor dem Schließen, damit die Tabelle hinter dem Drawer die Änderung zeigt. Die Wahl zwischen Server Actions und API Routes dafür ist eine eigene Entscheidung; wir haben die Optionen hier verglichen.

Slots mit unabhängigen Berechtigungen absichern

Parallel Routes geben auch eine saubere Antwort auf eine Berechtigungsfrage, die sonst in Prop Drilling endet: Was passiert, wenn Regionen einer Seite unterschiedliche Zugriffsregeln haben? Der klassische Fall ist ein Admin-Dashboard, in dem alle die Übersicht sehen, aber nur Admins das Audit-Log.

Weil das Layout die Slots als Props erhält, kann es entscheiden, welche es mountet:

// app/dashboard/layout.tsx
import { getSession } from '@/lib/auth'

export default async function DashboardLayout({
  children,
  metrics,
  auditlog,
}: {
  children: React.ReactNode
  metrics: React.ReactNode
  auditlog: React.ReactNode
}) {
  const session = await getSession()
  return (
    <div className="grid grid-cols-3 gap-6">
      <main className="col-span-2">{children}</main>
      <div className="space-y-6">
        {metrics}
        {session.role === 'admin' ? auditlog : null}
      </div>
    </div>
  )
}

Der Audit-Log-Slot sollte die Rolle in seiner eigenen Page noch einmal prüfen, bevor er Daten anfasst, denn eine Slot-Page ist eine unabhängige Server Component und günstig abzusichern. Das ergibt zwei Schichten, ohne ein User-Objekt durch Component Props zu fädeln. Middleware schützt weiterhin die Route als Ganzes; Slot-Checks übernehmen die Regeln auf Regionsebene, die Middleware nicht sehen kann.

Wann sich diese Patterns lohnen

Eine berechtigte Frage ist, ob die Ordnerakrobatik sich rechtfertigt. Unsere Daumenregel aus der Kundenarbeit:

Next.js Parallel Routes zahlen sich aus, wenn eine Seite Regionen mit unabhängigen Datenquellen, Fehlerbildern oder Berechtigungen enthält. Dashboards und Ops-Konsolen, hauptsächlich. Für eine Marketing-Seite oder ein Settings-Formular sind sie Overkill, und ein schlichter Komponentenbaum bleibt leichter lesbar.

Interception Routes zahlen sich in dem Moment aus, in dem ein Modal eine URL braucht. Zeigt der Drawer einen Datensatz, auf den jemand verlinken könnte, dann intercepte. Ein Bestätigungsdialog, den niemand je verlinken wird, braucht keine Route, und schlichter Component State bleibt dort das richtige Werkzeug.

Beide Features setzen den App Router voraus. Wenn du noch auf dem Pages Router bist oder mitten in der Migration steckst, sind URL-adressierbare Modals eines der besseren Argumente, den Umzug abzuschließen; wir haben einen inkrementellen Migrationsansatz beschrieben, der einen Big-Bang-Rewrite vermeidet.

Die Teams, die sich verbrennen, übernehmen die Konventionen nur halb: Slots ohne default.tsx-Dateien, Interception ohne Vollseiten-Fallback, Drawer, die Daten mutieren und die Liste darunter nie revalidieren. Diese Konventionen funktionieren als Paket. Übernimm das ganze Paket, und der Router trägt den State, den vorher deine Komponenten getragen haben.

Wir bauen und auditieren App-Router-Codebasen für B2B-SaaS-Teams im Rahmen unserer Arbeit in Webanwendungsentwicklung und individueller Softwareentwicklung. Wenn dein Dashboard zu einer Modal-State-Maschine gewachsen ist, die niemand anfassen will, schreib an hello@wolf-tech.io oder schau auf wolf-tech.io vorbei. Ein kurzes Gespräch über deinen Routing-Baum reicht meist, um zu sehen, wie viel davon der Router übernehmen könnte.