Dieser Artikel ist auf Englisch veröffentlicht.
Express-APIs in Route-Handler des Next.js App Router migrieren
Erfahren Sie, wie Sie Express-Routen, Middleware sowie Datenmuster mithilfe von Server Components in den Next.js App Router umwandeln und welche Aspekte der Bereitstellung zu berücksichtigen sind.
Wann macht der Umstieg auf Next.js tatsächlich Sinn (und wann nicht)
Der Next.js App Router ist eine sinnvolle Wahl, wenn Ihr Projekt mindestens zwei dieser Bedingungen erfüllt:
- Die Ladezeit beim ersten Aufruf oder die Sichtbarkeit in Suchmaschinen ist entscheidend und Sie betreiben derzeit eine Single-Page-Anwendung, die nach dem Laden der Seite Daten von einem Express-Backend abruft.
- Ihr Deployment-Pipeline zielt bereits auf Vercel oder ähnliche Plattformen ab oder Sie sind bereit, diese Infrastruktur zu übernehmen – die Edge- und Serverless-Funktionen des App Routers setzen dieses Hosting-Umfeld voraus.
- Ihre Express-Anwendung dient hauptsächlich der Bereitstellung von Seiten und erledigt standardmäßige CRUD-Operationen anstelle der Verwaltung persistenter Verbindungen, Hintergrundaufgaben oder rechenintensiver Prozesse.
Eine Migration ist nicht ratsam, wenn:
- Ihr Backend erhebliche Aufgaben über das bloße Rendern von Seiten bewältigt – z. B. Message Queues, geplante Aufgaben, gRPC-Endpunkte oder persistente Socket-Verbindungen. Die Route-Handler in Next.js können keinen dedizierten Backend-Dienst ersetzen; Sie werden wahrscheinlich Next.js als Frontend-Schicht einsetzen und gleichzeitig einen separaten Express- oder Node-Dienst dahinter laufen lassen.
- Man ist stark von einem etablierten Express-Middleware-Ökosystem abhängig – spezialisierte Authentifizierungsanbieter, Bibliotheken zur Limitierung der Anfragenrate oder Integrationen für die Überwachbarkeit – wofür eine vollständige Neuentwicklung nur geringe Vorteile bringen würde.
- Die Anwendung besteht hauptsächlich aus authentifizierten, interaktiven Dashboard-Oberflächen mit begrenzten Anforderungen an Suchmaschinen – die Hauptvorteile des App Routers (Server-Rendering per Streaming, Suchoptimierung, statische Generierung) bringen in diesem Szenario kaum Nutzen, während die Lerninvestition weiterhin erheblich ist.
Eine empfohlene Vorgehensweise: Behalten Sie Ihre Express-API als autoritatives Quellenmaterial für Daten und Geschäftsregeln bei und nutzen Sie Next.js als Rendering- sowie Backend-for-Frontend-Schicht. Migrieren Sie zunächst die Benutzeroberfläche sowie die auf Lesevorgänge ausgerichteten Routen, während die schreibintensiven internen Dienste unverändert bleiben. Diese Anleitung folgt dieser Migrationsstrategie.
Kopplung von Express-Routen an Next.js-Route-Handler
Route-Handler, die sich in app/api/**/route.ts befinden, bieten den direktesten Ersatz für Express-Routendefinitionen. Der konzeptionelle Unterschied besteht darin, dass anstelle der req- und res-Objekte ein Request übergeben wird und ein Response zurückgegeben wird (oder NextResponse zur besseren Verwendbarkeit genutzt wird), wobei jede HTTP-Methode zu einer separaten exportierten Funktion wird statt zu einem Aufruf von router.get().
Betrachten Sie einen Standard-Express-Route, der Bestellungen abruft und erstellt:
// express: routes/orders.ts
import ⟨0⟩ from „express“;
import ⟨1⟩ from „../db“;
import ⟨2⟩ from „../middleware/auth“;
const router = Router(); router.get(\"/api/orders\", requireAuth, async (req, res) => {
const userId = req.user.id;
const orders = await db.order.findMany({ where: ⟨3⟩ });
res.json⟨4⟩);
}); router.post(\"/api/orders\", requireAuth, async (req, res) => {
const ⟨5⟩ = req.body;
if (!items?.length) {
return res.status(400).json⟨6⟩);
}
const order = await db.order.create({
data: ⟨7⟩,
});
res.status(201).json⟨8⟩);
}); export default router;
Die entsprechende Implementierung des Route-Handlers:
// app/api/orders/route.ts
import ⟨0⟩ from „next/server“;
import ⟨1⟩ from „@/lib/db“;
import ⟨2⟩ from „@/lib/auth“;
export async function GET(req: NextRequest) {
const user = await getSessionUser(req);
if (!user) {
return NextResponse.json(⟪3⟩, ⟨4⟩);
} const orders = await db.order.findMany({ where: ⟨5⟩ });
return NextResponse.json(⟪6⟩);
}export async function POST(req: NextRequest) {
const user = await getSessionUser(req);
if (!user) {
return NextResponse.json(⟪7⟩, ⟨8⟩);
} const body = await req.json();
if (!body.items?.length) {
return NextResponse.json(⟪9⟩, { status: 400 }); } const order = await db.order.create({
data: { userId: user.id, items: body.items },
});
return NextResponse.json({ order }, { status: 201 });
}
Mehrere Details verdienen Aufmerksamkeit bei der Übersetzung von Express-Mustern in Route-Handler:
Dynamische Segmente verwenden eine auf Verzeichnissen basierende Syntax anstelle von Kolonenvorzeichen. Ein Express-Route-Pfad wie /api/orders/:id wird zu app/api/orders/[id]/route.ts. Der Parameter wird als zweites Argument an Ihre Handler-Funktion übergeben: GET(req, { params }: { params: Promise<{ id: string }> }). Aktuelle Next.js-Versionen liefern die params als Promise, sodass Sie darauf warten müssen, bevor Sie den Wert lesen können.
Es existiert keine Middleware-Kette pro Route. Die requireAuth-Middleware von Express wird entweder in eine gemeinsame Hilfsfunktion umgewandelt, die zu Beginn jedes Handlers aufgerufen wird (wie im obigen Beispiel gezeigt), oder besser noch in Logik innerhalb von middleware.ts (wie in einem späteren Abschnitt erläutert), sodass die einzelnen Route-Handler nichts von den Authentifizierungsaspekten wissen.
Die Analyse des Anfragekörpers erfordert explizite Aufrufe – man schreibt await req.json() anstelle davon, sich auf express.json() zu verlassen. Es findet keine automatische Analyse statt, was tatsächlich die Klarheit verbessert: Man umgeht unerwartete Grenzen bezüglich der Größe des Anfragekörpers, die durch eine globale Middleware verursacht werden könnten, die man vor Monaten konfiguriert hat und vergessen hat.
Route-Handler bleiben gewöhnliche Node- oder Edge-Funktionen. Wenn Sie die Eingaben in Ihren Express-Routen mit zod validiert haben, wird dieser Validierungscode unverändert übernommen.
Server-Komponenten im Vergleich zu Ihren bestehenden client-seitig rendernden React-Mustern
Dieser Aspekt überrascht Teams am meisten von allen. In einer Express-Plus-React-Architektur gilt jede Komponente standardmäßig als Client-Komponente: Sie wird im Browser rendernt, und wenn sie Daten benötigt, ruft sie Ihre API über useEffect oder durch eine Datenabruf-Bibliothek wie React Query auf.
// Altes Muster: vom Client gerendertes React, das mit Express kommuniziert
function OrderList() {
const [orders, setOrders] = useState<Order[] | null>(null);
useEffect(() => {
fetch("/api/orders", { credentials: "include" })
.then((r) => r.json())
.then((data) => setOrders(data.orders));
}, []); if (!orders) return <Spinner />;
return (
<ul>
{orders.map((o) => (
<li key={o.id}>{o.id} — ${o.total}</li>
))}
</ul>
);
}
Der App Router kehrt diese Standardkonfiguration um. Jede Komponente ist eine Server-Komponente, es sei denn, Sie geben etwas anderes an, was bedeutet, dass sie auf dem Server ausgeführt wird, direkt auf Ihre Datenbank oder Dienste zugreift und ihren JavaScript-Code niemals an den Client sendet. Für Daten, die zur jeweiligen Seite gehören, können Sie die API-Route ganz weglassen.
und fragen Sie direkt danach:// app/orders/page.tsx — Serverkomponente, kein „use client“
import { db } from "@/lib/db";
import { getSessionUser } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function OrdersPage() {
const user = await getSessionUser();
if (!user) redirect("/login"); // direkter DB-Zugriff, kein Fetch, kein Ladezustand, keine Kosten durch Client-Bundle
const orders = await db.order.findMany({
where: { userId: user.id }rver liefert HTML, das bereits die Daten enthält. Sie fügen "use client" nur dann hinzu, wenn eine Komponente Interaktivität erfordert: Zustand, Effekte, Ereignishandler oder nur im Browser verfügbare APIs:// app/orders/OrderFilters.tsx
"use client";
import > from "react";
import > from "next/navigation"; export function OrderFilters() {
const router = useRouter();
const params = useSearchParams();
const [status, setStatus] = useState(params.get("status") ?? "all"); function apply(next: string) {
setStatus(next);
const url = new URLSearchParams(params);
url.set("status", next);
router.push(`/orders?$>`);
} return (
<select value=> onChange=>>
<option value="all">Alle</option>
<option value="pending">Ausstehend</option>
<option value="shipped">>Geschickt</option>
</select>
);
}
/code> Die praktische Richtlinie für Teams: setzen Sie „use client“ so tief wie möglich im Komponentenbaum ein. Behalten Sie Ihre Seiten und Layouts als Server Components bei; wenden Sie die Anweisung nur auf solche Endkomponenten an, die tatsächlich Interaktivität benötigen. Wenn Sie aus Gewohnheit „use client“ in jede Komponente einfügen (was beim Portieren einer Single-Page-Anwendung ohne Neubetrachtung der Architektur passiert), haben Sie keinen Vorteil vom App Router und erhalten letztendlich ein schlechteres Verständnis der Struktur als bei der ursprünglichen Konfiguration.
Austausch des Express-Auth-Middlewares durch Next.js-Middleware
In Express läuft das Authentifizierungs-Middleware für jede Route innerhalb des Node.js-Prozesses. Next.js bietet middleware.ts an, das Anfragen in der Edge-Schicht abfängt, bevor sie eine Seite oder einen Handler erreichen, und somit die architektonische Entsprechung darstellt:
// alt: middleware/auth.ts (Express)
import jwt from „jsonwebtoken“;
export function requireAuth(req, res, next) {
const token = req.cookies.session;
if (!token) return res.status(401).json([„0“]); try {
req.user = jwt.verify(token, process.env.JWT_SECRET!);
next();
} catch {
res.status(401).json([„1“]);
}
}
// middleware.ts – befindet sich im Projektverzeichnis
import „2“ from „next/server“;
import „3“ from „jose“; // kompatibel mit Edge, im Gegensatz zu jsonwebtoken
const PROTECTED_PREFIXES = [„/dashboard“, „/orders“, „/api/orders“]; export async funktionion middleware(req: NextRequest) {
const isProtected = PROTECTED_PREFIXES.some((p) =>
req.nextUrl.pathname.startsWith(p)
);
if (!isProtected) return NextResponse.next(); const token = req.cookies.get("session")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", req.url));
} try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
const { payload } = await jwtVerify(token, secret); // forward the verified user id downstream via a request header
const headers = new Headers(req.headers);
headers.set("x-user-id", String(payload.sub));
return NextResponse.next({ request: { headers } });
} catch {
return NextResponse.redirect(new URL("/login", req.url));
}
}export const config = {
matcher: ["/dashboard/:path*", "/orders/:path*", "/api/orders/:path*"],
};
Zwei
Probleme, die Teams bei der Migration zuverlässig behindern:- Standardmäßig läuft das Middleware auf dem Edge-Runtime, nicht auf Node.js. Jede Bibliothek, die von Node.js-Kernmodulen abhängt – wie
jsonwebtokenoder gängige Datenbankklienten – wird ausfallen oder stillschweigend fehlfunktionieren. Wählen Sie stattdessen edge-fähige Alternativen:joseist die übliche Wahl für JWT-Operationen. Verlegen Sie alle Datenbankabfragen in Server Components oder Route Handlers, wo die vollständige Node.js-Umgebung vorhanden ist; versuchen Sie niemals, sie im Middleware auszuführen.
SELECT * FROM sessions WHERE id = ? ausführen können. Beschränken Sie die Middleware-Logik auf schnelle, zustandslose Operationen wie die Überprüfung der Token-Signatur. Verschieben Sie Autorisierungsfragen – „Kann dieser Benutzer diese Bestellung ansehen?“ – auf die Seite selbst oder den zugehörigen Handler, wo Zugriff auf die Datenbank sowie Node.js-APIs vollständig verfügbar sind.Datenabrufmuster: Server Components gegen Ihren aktuellen API-Aufruf-Ansatz
Die meisten Express-Plus-Reakt-Architekturen folgen einer vorhersehbaren Abfolge: Der Component wird geladen, ruft Ihre API auf, die API fragt die Datenbank ab, das JSON wird zurück zum Browser gesendet und der Component aktualisiert sich. Zwei Netzwerkwege – vom Client zum Server, vom Server zur Datenbank – liefern Informationen, die der Server bereits besaß.
Mit dem App Router komprimieren Lesevorgänge in Server Components diesen Prozess auf einen einzigen Schritt: Der Server fragt die Datenbank ab und streamt den HTML-Inhalt mit dem Ergebnis direkt an den Client, wie im vorherigen OrdersPage-Code gezeigt. Für Schreibvorgänge gibt es zwei gängige Ansätze: Route Handlers, wenn Sie eine herkömmliche API benötigen (zum Beispiel eine öffentliche REST-Schnittstelle), oder Server Actions für Änderungen, die durch eigene Formulare und Benutzeroberflächen ausgelöst werden.
Server Actions weichen am stärksten von den Express-Konventionen ab. Sie definieren eine Funktion, die auf dem Server ausgeführt wird, und rufen sie direkt von einem Formular aus auf, ohne eine explizite API-Route erstellen zu müssen:
// app/orders/actions.ts
"use server";
import { db } from "@/lib/db";
import { getSessionUser } from "@/lib/auth";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";export async function createOrder(formData: FormData) {
const user = await getSessionUser();
if (!user) redirect("/login"); const itemId = formData.get("itemId");
if (typeof itemId !== "string" || !itemId) {
throw new Error("itemId ist erforderlich");
} await db.order.create({
data: { userId: user.id, items: [{ itemId, qty: 1 }] },
}); // Neuaufladen der Orders-Seite mit aktuellen Serverdaten — kein erneutes Abrufen vom Client nötig
revalidatePath("/orders");
}
// app/orders/NewOrderForm.tsx
import ⟨0⟩ from „./actions“;
export function NewOrderForm() {
return (
<form action=⟪1⟩>
<input type="text" name="itemId" placeholder="Item ID" required />
<button type="submit">Bestellung erstellen</button>
</form>
);
}
Auswahl zwischen Server Actions und Route Handlers:
Eine Server Action ist ein interner Funktionsaufruf von der Benutzeroberfläche zum Server; ein Route Handler ist ein eigentlicher HTTP-Endpunkt. Der Unterschied ist wichtig, wenn man entscheiden muss, welches Muster zu einer bestimmten Änderung passt.
Nutzen Sie einen Route Handler, wenn der Endpunkt stabil sein muss und von außerhalb dieser Next.js-Anwendung aufgerufen werden kann – beispielsweise von einem mobilen Client, einer Integration Dritter oder einer öffentlichen API. Route Handler bieten Ihnen explizite URL-Pfade, HTTP-Verben sowie einen von Ihnen kontrollierten und versionierten Vertrag.
Nutzen Sie eine Server Action, wenn die Änderung von Ihren eigenen Formularen und interaktiven Komponenten ausgeht. Server Actions erfordern weniger Standardcode und validieren Daten im Cache automatisch über revalidatePath oder revalidateTag, wodurch das manuelle Cache-Busting, das nach einem fetch-Aufruf notwendig wäre, entfällt. Sie sind nicht für externe Nutzer oder Garantien der Rückwärtskompatibilität konzipiert; betrachten Sie sie als interne Remote-Procedure-Calls.
Der praktische Test: Wenn Sie einen Endpunkt für jemanden außerhalb Ihres Teams dokumentieren möchten, machen Sie daraus einen Route Handler. Wenn er nur dazu dient, eine Schaltfläche oder ein Formular in Ihrer Benutzeroberfläche zu unterstützen, ist eine Server Action einfacher.
Unterschiede bei der Bereitstellung (Vercel vs ECS/EB)
Teams, die daran gewöhnt sind, Express auf ECS oder Elastic Beanstalk zu bereitstellen, stoßen mit Next.js auf Vercel auf einen grundlegenden architektonischen Wandel. Eine Express-Anwendung läuft als lang lebender Node-Prozess: Ein Prozess handhabt viele Anfragen, hält Datenbankverbindungen aktiv und weist vorhersehbare Merkmale bezüglich Speicherbedarf und Startzeit auf.
Next.js auf Vercel deployt Seiten und Route Handlers als einzelne serverlose oder Edge-Funktionen. Jede Funktion startet unabhängig neu, arbeitet innerhalb ihrer eigenen Ausführungsgrenzen und teilt sich keinen persistenten Datenbankverbindungspool, wie es bei einem einzigen Express-Prozess der Fall ist. Wenn Sie einen Prisma-Client auf dieselbe Weise initialisieren wie in Express, wird Ihr Datenbankverbindungslimit unter Last erschöpft, da jede Funktionsaufrufung ihre eigene Verbindung erstellen kann.
// lib/db.ts – erforderliches Muster für serverless Prisma
import { PrismaClient } from "@prisma/client";
const globalForPrisma = global as unknown as { prisma: PrismaClient }; // Wiederverwendung des Clients bei wiederholten Aufrufen, anstatt jedes Mal einen neuen zu erstellen
export const db =
globalForPrisma.prisma ??
new PrismaClient({
// Verwendung einer gepoolten Verbindungszeichenkette (z. B. PgBouncer / Prisma Accelerate / RDS Proxy)
datasources: { db: { url: process.env.DATABASE_URL_POOLED } },
});if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
Dieses Muster wiederverwendet den Client bei wiederholten Aufrufen, anstatt bei jeder Anfrage eine neue Instanz zu erstellen.
Falls Sie Next.js auf ECS oder Elastic Beanstalk anstelle von Vercel bereitstellen, funktioniert das Framework wie ein herkömmlicher Node-Server. Führen Sie zuerst next build aus, gefolgt von next start, oder konfigurieren Sie den Standalone-Ausgabemodus, um kompaktere Docker-Container zu erzeugen. Sie behalten die Vorteile langlebiger Prozesse, verlieren jedoch Vercels automatisches Edge-Netzwerk, die schrittweise statische Regeneration im CDN sowie die vorkonfigurierten Vorabveröffentlichungen. Sie müssen diese Funktionen selbst konfigurieren oder deren Fehlen in Kauf nehmen. Dies ist eine legitime ingenieurtechnische Entscheidung, kein Kompromiss; viele Teams nutzen Next.js auf ECS gerade deshalb, weil sie bereits die Infrastruktur besitzen und es vorziehen, die Hosting-Lösung nicht bei zwei Anbietern aufzuteilen.
Nehmen Sie Zeit für zwei weitere Migrationsaufgaben ein: Umgebungsvariablen (Next.js erfordert das Präfix NEXT_PUBLIC_ für jede Variablen, die dem Browser zur Verfügung gestellt wird – überprüfen Sie alle Variablen, die Sie in Ihre React-Kompilierung einbeziehen), sowie Konfiguration zur Kompilierzeit versus Laufzeit (Werte, die in eine statische Exportdatei eingebettet sind, verhalten sich anders als Werte, die zur Zeit der Anfrage gelesen werden, im Gegensatz zu einem einzelnen Express-Prozess, bei dem alles in der Laufzeit abgewickelt wird).
Migrations-Checkliste und häufige Fehlerquellen
Folgen Sie diesen Schritten in der richtigen Reihenfolge:
- Deployen Sie die Next.js-Anwendung parallel zu Ihrem Express-Server – lassen Sie die bestehende Anwendung bei der initialen Einrichtung unberührt.
- Übersetzen Sie zunächst die stark lesensintensiven, für Suchmaschinen optimierten Seiten – diese Seiten profitieren am meisten von Server Components und weisen ein minimales Migrationsrisiko auf.
middleware.ts mit einer Token-Verifikationsbibliothek ein, die mit dem Edge-Runtime kompatibel ist.next build && next start) – next dev maskiert bestimmte Fehler, die nur im Produktionsmodus auftreten, wie beispielsweise Verstöße gegen die Trennung zwischen Server und Client.Häufige Fehler bei der Migration:
- Importieren von serverexklusivem Code in eine Client-Komponente. Wenn eine
"use client"-Komponente etwas importiert, das auffs, den Datenbankclient oder Geheimnisse zugreift, fehlschlägt der Build – oder schlimmer noch: Die Geheimnisse werden stillschweigend in die Client-JavaScript-Datei eingebunden. Installieren Sie dasserver-only-Paket, um einen Buildfehler auszulösen, anstatt sensible Daten heimlich weiterzugeben.
revalidatePath oder revalidateTag nach einer Server-Aktion. Ohne explizite Neuvalidierung zeigt die Schnittstelle nach einer Änderung veraltete Daten an, da Server-Komponenten im Cache liegen können."use client" kennzeichnen. Gewohnheiten aus Single-Page-Anwendungen sind der Hauptgrund dafür, dass eine Next.js-Anwendung nicht besser abschneidet als die ursprüngliche.Zusammenfassung
Falls Sie weiterhin unsicher sind, beginnen Sie mit einem begrenzten Experiment: Wählen Sie eine lesereiche, für SEO wichtige Seite aus Ihrer aktuellen Anwendung aus, bauen Sie sie als Server Component um, der auf Ihre Datenbank zugreift oder direkt Ihre vorhandene Express API aufruft, und deployen Sie sie an einer Route, die von Ihrem Express-Server nicht bearbeitet wird. Vergleichen Sie vor dem Migrieren weiterer Seiten die Zeit bis zum ersten Byte sowie die Größe des Bundles. Nachdem Sie diesen Ansatz überprüft haben, bringen Sie die Authentifizierung in middleware.ts unter, und konvertieren Sie anschließend Ihre am stärksten genutzten Workflow-Variablen nacheinander in Server Actions – eine schrittweise Migration ist sicherer und praktischer als ein kompletter Neuschreibprozess. Teams, die Probleme haben, sind jene, die den gesamten Codebase migrieren, bevor sie herausfinden, wo Server Components tatsächlich Arbeit sparen und wo sie lediglich eine neue konzeptionelle Schicht auf einem funktionierenden System einfügen.
Verwandte Artikel
- Eine React-Suchoberfläche mit klarer Zustandsverwaltung erstellen — Ereignisse, abgeleitete Werte, Lebenszyklen von Anfragen sowie die Anzeige von Ergebnissen getrennt halten, damit die richtige Antwort zur richtigen Abfrage gelangt.
- RAG durch Erkennung der ersten fehlerhaften Stufe debuggen — Fehler bei Quellen, Abrufvorgängen, Generierung sowie Betriebstätigkeiten voneinander trennen, damit jedes Experiment ein klar identifiziertes Problem behebt.
- Die Brückenfunktion von TypeScript 6 auf dem Weg zu einem nativen TS 7-Compiler — Erfahren Sie, wie TypeScript 6 Standardkonfigurationen, Modulauflösung und Importsyntax aktualisiert, um Codebasen auf den schnelleren, auf Go basierenden TypeScript 7-Compiler vorzubereiten.