Next.js Middleware in Produktion: Edge-Logik, die deine App nicht zerlegt

#Next.js Middleware Produktion
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Die meisten Teams schreiben ihre erste Middleware in zehn Minuten und verbringen die nächsten zwei Wochen damit herauszufinden, warum sie sich seltsam verhält. Next.js Middleware produktionsreif zu bekommen ist eine andere Aufgabe, als sie lokal zum Laufen zu bringen. Denn Middleware läuft in der Edge Runtime, einer eingeschränkten Umgebung, die Node.js-APIs, die meisten npm-Pakete und jede Debugging-Gewohnheit deines Teams ablehnt. Der Code wird vor jedem einzelnen Request ausgeführt, den deine App bekommt. Ein Fehler an dieser Stelle ist deshalb keine kaputte Seite. Es ist eine kaputte Anwendung.

Dieser Beitrag behandelt die Muster, die den Kontakt mit Produktions-Traffic überstehen: JWT-Verifikation ohne Nodes crypto-Modul, Matcher-Konfiguration, die Middleware von Routen fernhält, die sie nie berühren sollte, Auth-Redirects ohne Endlosschleifen und den Debugging-Workflow für eine Umgebung, in der console.log niemals dein Terminal erreicht.

Was die Edge Runtime wirklich ist

Next.js Middleware läuft nicht in deinem Node.js-Serverprozess. Sie läuft in der Edge Runtime, einer minimalen JavaScript-Umgebung, die an Web-APIs statt an Node-APIs orientiert ist. Ob sie physisch auf einem CDN-Edge-Node ausgeführt wird (Vercel, Cloudflare) oder in deinem selbst gehosteten Next.js-Server, die Einschränkungen der Runtime sind identisch.

Was du bekommst: fetch, Request, Response, URL, Headers, WebCrypto (crypto.subtle), TextEncoder und Structured Clone. Was du nicht bekommst: fs, net, child_process, Nodes crypto und Buffer (jenseits eines kleinen Polyfills) sowie jedes npm-Paket, das davon abhängt. Diese letzte Kategorie ist größer, als die meisten Teams erwarten. Datenbanktreiber, die meisten ORMs, jsonwebtoken, bcrypt und viele Logging-SDKs scheitern zur Build-Zeit oder, schlimmer, zur Laufzeit.

Das mentale Modell, das funktioniert: Middleware ist ein Request-Filter, kein Request-Handler. Sie inspiziert, schreibt um, leitet weiter und setzt Header oder Cookies. Alles Schwerere gehört in einen Route Handler oder eine Server Component.

JWT-Verifikation für Next.js Middleware in Produktion

Die häufigste Aufgabe von Middleware ist die Prüfung, ob ein Request eine gültige Session mitbringt, bevor er eine geschützte Route erreicht. Der häufigste Fehler ist der Griff zu jsonwebtoken, das von Nodes crypto abhängt und am Edge nicht läuft.

Die Bibliothek, die funktioniert, ist jose, aufgebaut auf WebCrypto:

import { jwtVerify } from 'jose';

const secret = new TextEncoder().encode(process.env.SESSION_SECRET);

export async function verifySession(token: string) {
  try {
    const { payload } = await jwtVerify(token, secret, {
      algorithms: ['HS256'],
    });
    return payload;
  } catch {
    return null;
  }
}

Zwei Produktionshinweise zu diesem Muster. Erstens: Pinne die Algorithmus-Liste. Lässt du algorithms weg, verlässt du dich darauf, dass der Token-Header seinen eigenen Algorithmus deklariert, und genau das ist die Wurzel einer bekannten Klasse von JWT-Angriffen. Zweitens: Verifikation in der Middleware sollte günstig und zustandslos sein. Prüfe Signatur und Ablauf, lies die Claims, die du fürs Routing brauchst, und hör auf. Zu prüfen, ob der Nutzer noch existiert oder ob das Abo aktiv ist, braucht eine Datenbank, und die Datenbank ist genau das, was Middleware nicht erreichen kann. Überlass diese Prüfung der Seite oder der API-Route. Middleware filtert nur Requests aus, die eindeutig nicht authentifiziert sind.

Matcher-Konfiguration: Middleware von Routen fernhalten, die sie nichts angehen

Standardmäßig läuft Middleware bei jedem Request: Seiten, API-Routen, Prefetches, Bilder, Schriften und jede Datei in /_next/static. Session-Logik gegen eine Schriftdatei laufen zu lassen verbrennt Rechenzeit und kostet auf nutzungsbasiert abgerechneten Edge-Plattformen echtes Geld. Die matcher-Konfiguration ist die Lösung, und ihre zuverlässigste Produktionsform ist ein Negative Lookahead:

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|.*\\.(?:png|jpg|jpeg|svg|webp|ico|woff2)).*)',
  ],
};

Das schließt API-Routen, Next.js-Interna und statische Assets in einem einzigen Ausdruck aus. Passe die Ausschlussliste an deine App an. Das Prinzip lautet: Middleware soll bei Navigationen laufen, nicht bei Asset-Requests.

Zwei Einschränkungen solltest du kennen, bevor sie dich treffen. Matcher-Muster müssen statisch analysierbar sein. Du kannst sie nicht zur Laufzeit aus Variablen zusammenbauen, weil Next.js sie zur Build-Zeit extrahiert. Und der Matcher greift auf den eingehenden Request-Pfad, nicht auf den umgeschriebenen. Ein Rewrite innerhalb der Middleware löst den Matcher also nicht erneut aus.

Wenn dein Matcher kompliziert wird, weil verschiedene Route-Gruppen unterschiedliche Logik brauchen, behalte einen Matcher und verzweige innerhalb der Middleware-Funktion über request.nextUrl.pathname. Ein Einstiegspunkt mit expliziter Verzweigung lässt sich deutlich leichter prüfen als eine subtile Regex.

Auth-Redirects ohne Endlosschleifen

Der klassische Middleware-Fehler ist die Redirect-Schleife: Nicht authentifizierte Nutzer werden auf /login geschickt, die Middleware läuft dann auf /login, findet keine Session und leitet erneut auf /login weiter. Browser stoppen das nach ein paar Dutzend Sprüngen. Dein Error-Tracking läuft lange vorher voll.

Das robuste Muster definiert öffentliche Pfade explizit und prüft sie vor jeder Auth-Logik:

const PUBLIC_PATHS = ['/login', '/register', '/forgot-password'];

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) {
    return NextResponse.next();
  }

  const token = request.cookies.get('session')?.value;
  if (!token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}

Der from-Parameter ist wichtig für die Nutzererfahrung, weil du Leute nach dem Login dorthin zurückbringen kannst, wo sie waren. Aber validiere ihn, bevor du dorthin weiterleitest. Akzeptiere nur relative Pfade, die mit einem einzelnen / beginnen, sonst hast du einen Open Redirect in deinen Auth-Flow eingebaut.

Die zweite Schleifenquelle ist subtiler: eine Weiterleitung auf eine Route, die selbst in einer Server Component oder einem Route Handler weiterleitet. Middleware-Redirects und Redirects auf Anwendungsebene beeinflussen sich gegenseitig, und keiner weiß vom anderen. Halte die Redirect-Entscheidung in einer Schicht. Wenn die Middleware die Auth-Redirects besitzt, soll nichts anderes aus Auth-Gründen weiterleiten.

Die Performance-Kosten, die niemand einplant

Middleware läuft vor jedem passenden Request, seriell, vor dem Rendering. Ihre Latenz wird bei jeder Seite, die sie berührt, auf deine Time to First Byte addiert. Eine schnelle Middleware, die ein Cookie liest, ein JWT verifiziert und einen Header setzt, kostet eine bis drei Millisekunden. Eine Middleware, die per fetch einen externen Dienst aufruft, kostet bei jedem Request genau so viel, wie dieser Roundtrip kostet.

Für dieses letzte Muster gilt eine harte Regel: keine blockierenden fetch-Aufrufe in der Middleware, außer der Endpunkt ist genau dafür ausgelegt. Ein Session-Lookup gegen dein Backend bei jedem Request bedeutet, dass deine gesamte Seite nur noch so schnell ist wie das p99 dieses Endpunkts. Teams tun das, um "echte" Session-Validierung am Edge zu bekommen, und addieren am Ende 80 bis 300 Millisekunden auf jeden Seitenaufruf. Wenn du stärkere Validierung brauchst, als ein signiertes JWT bietet, nutze kurze Token-Laufzeiten mit Rotation statt eines Backend-Aufrufs pro Request.

Miss es: performance.now() funktioniert in der Edge Runtime, und wenn du die Differenz am Ende der Middleware-Funktion in deine Edge-Logs schreibst, bekommst du eine Latenzverteilung für die Middleware selbst, getrennt vom Rendering.

Debugging: Wo ist console.log geblieben?

Lokal erscheinen Middleware-Logs in deinem next dev-Terminal und alles fühlt sich normal an. In Produktion auf einer Edge-Plattform landet die Ausgabe von console.log nicht in deinen Server-Logs. Auf Vercel landet sie in den Edge-Function-Logs pro Request. Selbst gehostet geht sie in den stdout des Node-Prozesses, vermischt mit allem anderen und ohne Request-Kontext.

Der Workflow, der in Produktion funktioniert, hat drei Teile. Erstens strukturierte einzeilige Logs: ein JSON-Objekt pro Request mit dem Pfad, der Entscheidung (next, redirect, rewrite) und dem Grund, damit du Entscheidungen filtern und zählen kannst, statt Prosa zu lesen. Zweitens Entscheidungs-Header in Nicht-Produktionsumgebungen: Setzt du x-middleware-decision: redirect-no-session auf die Response, wird das Verhalten im Network-Tab der Browser-DevTools sichtbar, ganz ohne Log-Zugriff. Drittens die Edge-Einschränkungen lokal reproduzieren, mit next dev plus next build. Ein Paket, das nur in der Edge Runtime bricht, bricht meist schon zur Build-Zeit. Behandle einen sauberen Production-Build deshalb als Teil deiner Middleware-Testschleife.

Was Middleware nicht kann, und was du stattdessen tust

Die Einschränkungen, gesammelt an einer Stelle. Keine Datenbankabfragen: In der Runtime existieren kein ORM, kein Treiber und keine Primitive für Connection Pooling. Validiere stattdessen signierte Tokens oder schreibe auf einen Route Handler um, der den Lookup übernimmt. Keine Node-only-npm-Pakete: Prüfe, ob eine Abhängigkeit Edge-Runtime-Unterstützung deklariert, bevor du sie in die Middleware importierst. jose, @upstash/redis (HTTP-basiert) und einfache fetch-Clients funktionieren, pg, mysql2, ioredis und jsonwebtoken nicht. Keine Erzeugung von Response Bodies: Middleware kann weiterleiten, umschreiben und Header setzen, aber HTML oder JSON zu produzieren gehört in Handler. Keine lang laufende Arbeit: Plattformen begrenzen die CPU-Zeit von Middleware aggressiv, und eine langsame Middleware verzögert jeden Request dahinter.

Nichts davon ist willkürlich. Genau das hält die Schicht vor dem Request schnell genug, um bei jedem Request zu laufen. Gegen die Einschränkungen anzuarbeiten erzeugt fragile Systeme. Innerhalb von ihnen zu entwerfen erzeugt Middleware, über die du irgendwann nicht mehr nachdenkst.

Eine kommentierte Produktions-Middleware

Hier die Form einer Middleware, die wir für ein Symfony-gestütztes Next.js-SaaS mit Subdomain-Tenant-Routing ausliefern. Das Next.js-Frontend spricht mit einer Symfony-API, und das Session-Cookie ist ein vom Backend ausgestelltes JWT:

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 1. Öffentliche Pfade: keine Auth-Logik, keine Schleifen.
  if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) {
    return NextResponse.next();
  }

  // 2. Tenant-Auflösung aus der Subdomain, per Header an die App.
  const host = request.headers.get('host') ?? '';
  const tenant = host.split('.')[0];

  // 3. Zustandslose Session-Prüfung: nur Signatur und Ablauf.
  const token = request.cookies.get('wt_session')?.value;
  const session = token ? await verifySession(token) : null;

  if (!session) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    return NextResponse.redirect(loginUrl);
  }

  // 4. Claims reisen über Request-Header; Seiten prüfen Kritisches erneut.
  const headers = new Headers(request.headers);
  headers.set('x-tenant', tenant);
  headers.set('x-user-id', String(session.sub));

  return NextResponse.next({ request: { headers } });
}

Die nummerierten Kommentare sind die Architektur: öffentliche Pfade zuerst, günstige Tenant-Auflösung, zustandslose Verifikation und Claims, die per Header weitergereicht werden, damit Server Components sie mit headers() lesen statt das Cookie erneut zu parsen. Das Symfony-Backend bleibt die Quelle der Wahrheit. Die Middleware entscheidet nie, was ein Nutzer darf, sondern nur, ob der Request es wert ist, dafür zu rendern.

Diese Arbeitsteilung zwischen einer Next.js-Edge-Schicht und einer Backend-API ist ein wiederkehrendes Thema in unserer Arbeit rund um Webanwendungsentwicklung, und genau diese Grenze zu prüfen, also was am Edge läuft und was dem Backend gehört, ist fester Bestandteil unserer Code-Audits. Es passt natürlich zu den Caching-Entscheidungen aus Next.js 15 Caching- und Revalidierungsmuster.

Ausliefern ohne Überraschungen

Middleware belohnt Teams, die sie wie Infrastruktur behandeln statt wie Anwendungscode: klein, zustandslos, gemessen und langweilig. Verifiziere Signaturen mit jose, begrenze die Ausführung mit einem Negative-Lookahead-Matcher, definiere öffentliche Pfade vor der Auth-Logik, halte blockierende Aufrufe aus dem heißen Pfad und logge Entscheidungen als strukturierte Daten.

Wenn deine Middleware zu etwas gewachsen ist, das niemand mehr anfassen will, oder wenn du ein Auth- oder Multi-Tenant-Setup planst und die Edge-Schicht gleich beim ersten Mal richtig entworfen haben willst: Wir machen das bei individuellen Softwareprojekten regelmäßig. Melde dich unter hello@wolf-tech.io oder über wolf-tech.io. Ein kurzer Blick auf deine Middleware- und Matcher-Konfiguration reicht meist, um die Überraschungen im Request-Pfad zu finden, bevor deine Nutzer sie finden.