
Next.js App Router vs Remix Data Fetching: Der knallharte Code-Vergleich

Next.js App Router vs Remix Data Fetching: Der knallharte Code-Vergleich
Erinnerst du dich noch an die dunklen Zeiten von useEffect? Du hast eine leere Komponente gerendert, einen Lade-Spinner gezeigt, dann per Fetch-API das Backend angepingt, gewartet, den State aktualisiert und schließlich die echten Daten gemalt. Was für ein architektonischer Albtraum. Wasserfälle aus Netzwerk-Requests, flackernde Layouts und grauenhaftes SEO waren die Folge.
Gott sei Dank sind wir heute weiter. Wir haben verstanden, dass Datenbeschaffung auf den Server gehört – nah an die Datenbank. Aber wenn es heute um die große Entscheidung Next.js vs Remix Data Fetching geht, prallen zwei völlig verschiedene Philosophien aufeinander.
Auf der einen Seite steht Vercels Next.js mit dem App Router und den viel diskutierten React Server Components (RSC). Hier schreibst du asynchronen Code direkt in deine UI-Komponente. Auf der anderen Seite steht Remix (inzwischen das Herzstück von React Router 7), das auf klassische, strikt getrennte loader-Funktionen setzt.
Wenn du aktuell in einem Projekt feststeckst und dich fragst: "Wie zur Hölle hole ich diese Liste von Usern am elegantesten aus meiner Postgres-Datenbank?", dann bist du hier exakt richtig. Wir lassen das theoretische Architektur-Gequatsche heute mal beiseite. Wir öffnen die IDE.
Wir bauen in diesem Artikel das exakt gleiche Feature in beiden Frameworks. Das Szenario ist simpel, aber extrem realitätsnah: Wir haben einen Tech-Blog. Wir wollen auf unserer Startseite die neuesten Artikel aus einer Datenbank laden. Als ORM nutzen wir das populäre Prisma, weil es 2026 schlichtweg der Standard in der TypeScript-Welt ist.
Welcher Code fühlt sich natürlicher an? Wo lauern die versteckten Performance-Fallen? Und welches mentale Modell rettet dir am Freitagabend um 17 Uhr den Verstand, wenn du einen fiesen Bug suchst?
Schnall dich an. Wir starten direkt mit Vercels Flaggschiff und schauen uns an, wie sich React Server Components anfühlen, wenn man sie auf eine echte Datenbank loslässt.

Next.js: Der "Es fühlt sich verboten an"-Ansatz
Okay, starten wir mit Next.js. Vercel hat mit dem App Router wirklich alles auf den Kopf gestellt. Als ich das erste Mal eine React Server Component (RSC) geschrieben habe, dachte ich ernsthaft, ich mache mich strafbar.
Warum? Weil uns jahrelang eine goldene Regel eingeprügelt wurde: Trenn dein Frontend vom Backend! Schreib eine saubere API-Route, hol dir die Daten per Fetch im Client und zeig solange einen hübschen Lade-Spinner. Vergiss das. Beim Thema Next.js vs Remix Data Fetching geht Next.js den radikalsten Weg. In Version 15 und 16 baust du deine Datenbankabfrage direkt in die UI. Kein Witz.
Lass uns direkt in den Code springen. Wir wollen die neuesten Artikel für unseren Tech-Blog aus einer Postgres-Datenbank laden. Wir nutzen dafür Prisma. So sieht deine app/page.tsx aus:
1import prisma from '@/lib/prisma';
2import { ArticleCard } from '@/components/ArticleCard';
3
4// Bam. Einfach eine async Funktion.
5export default async function BlogIndex() {
6
7 // Direkter Datenbankzugriff. Mitten in React.
8 const articles = await prisma.article.findMany({
9 orderBy: { createdAt: 'desc' },
10 take: 10,
11 });
12
13 return (
14 <main>
15 <h1>Neueste Tech-Artikel</h1>
16 <ul>
17 {articles.map((article) => (
18 <li key={article.id}>
19 <ArticleCard title={article.title} content={article.excerpt} />
20 </li>
21 ))}
22 </ul>
23 </main>
24 );
25}Schau dir das an. Da steht ein fettes, blockierendes await prisma.article.findMany() einfach mitten in der React-Komponente.
Als Frontend-Entwickler der alten Schule zuckt man da erstmal heftig zusammen. Man hat sofort Panik: "Moment, lade ich mir damit nicht die kompletten Prisma-Binaries und mein streng geheimes Datenbank-Passwort in den Browser des Nutzers?"
Nein. Und genau das ist der Mindblow bei Server Components. Dieser Code verlässt deinen Server niemals. Er wird knallhart auf der Serverseite ausgeführt, die Daten werden geholt, ins HTML gepresst und erst dann über die Leitung geschickt. Der Client sieht am Ende nur pures, dummes HTML. Deine Datenbank-Credentials sind absolut sicher.
Das Developer-Erlebnis hier ist absurd gut. Du sparst dir den kompletten API-Layer. Keine Fetch-Aufrufe, kein useEffect, kein State-Management für die Daten. Du hast durchgehend perfekte Typensicherheit. Wenn du in Prisma die Spalte excerpt in summary umbenennst, schreit dich dein VS Code unten in der Komponente sofort rot an.
Aber Moment. Machen wir uns nichts vor. Was passiert, wenn diese Datenbankabfrage mal zwei Sekunden dauert? Oder wenn dein Server gerade unter Last ächzt?
Bei diesem simplen Code passiert genau eines: Der Browser des Nutzers bleibt zwei Sekunden lang komplett weiß. Nichts lädt. Der Main-Thread blockiert. Und das ist für die Core Web Vitals der absolute Tod. Wie fixen wir das im Next.js-Universum?

Wir haben in Next.js also diesen dicken, fetten await-Block mitten in unserer Page-Komponente. Wenn die Postgres-Datenbank jetzt aber Schluckauf hat und zwei Sekunden braucht, starrt unser User exakt zwei Sekunden lang auf eine komplett weiße Seite. Der Browser lädt, aber es passiert visuell absolut nichts. In der UX-Welt ist das ein sofortiger Absprunggrund.
Früher hätten wir jetzt schnell einen State isLoading auf true gesetzt und einen Spinner im Client gerendert. Aber wir haben ja keinen State mehr! React Server Components laufen auf dem Server, und da gibt es schlichtweg kein useState.
Next.js löst dieses Problem über eine extrem clevere Dateisystem-Konvention. Du legst einfach exakt neben deine page.tsx eine neue Datei namens loading.tsx.
1// app/loading.tsx
2export default function Loading() {
3 return (
4 <div className="animate-pulse">
5 <h2>Lade neueste Tech-Artikel...</h2>
6 <div className="h-32 bg-gray-800 rounded-md mt-4"></div>
7 <div className="h-32 bg-gray-800 rounded-md mt-4"></div>
8 </div>
9 );
10}Was das Next.js-Framework hier unter der Haube abzieht, ist harte React-Magie. Das Framework nimmt deine page.tsx und wickelt sie vollautomatisch in eine <Suspense>-Boundary ein. Deine loading.tsx wird als Fallback-UI (also als Platzhalter) genutzt.
Das Resultat nennt sich Streaming. Wenn der User die Seite aufruft, schickt der Server sofort den Layout-Rahmen, die Navigation und deinen Skeleton-Loader aus der loading.tsx an den Browser. Die Seite ist instant da. Der Nutzer hat sofort Feedback.
Während der User den Skeleton-Loader betrachtet, rödelt der Server im Hintergrund weiter an dem schweren await prisma.article.findMany() Aufruf. Sobald die Datenbank die Artikel endlich ausspuckt, streamt Next.js die fertigen UI-Fragmente über die exakt selbe, noch offene HTTP-Verbindung hinterher und tauscht den Platzhalter nahtlos gegen die echten Daten aus.
Das fühlt sich beim Bauen unfassbar modern an. Du hast null Client-Side-JavaScript für diesen ganzen Prozess geschrieben. Keine wilden Fetch-Wasserfälle im Browser. Du machst dir keine Gedanken mehr über useEffect-Race-Conditions.
Aber... (und das ist ein massives Aber beim Thema Next.js vs Remix Data Fetching) ... dieses RSC-Modell hat auch seine echten Schattenseiten. Wenn du anfängst, zig Server Components tief ineinander zu verschachteln, die alle ihre eigenen Datenbank-Aufrufe absetzen, verlierst du rasend schnell den Überblick. Du baust dir plötzlich unbemerkt gewaltige Daten-Wasserfälle auf dem Server, weil tief verschachtelte Komponenten erst anfangen zu fetchen, wenn die Eltern-Komponente fertig gerendert ist. Das Debugging wird hier manchmal zum echten Blindflug.
Da loben sich viele Entwickler die absolute Vorhersehbarkeit und die strikte Trennung von Remix. Wie sieht also das genaue Gegengewicht zu diesem Next.js-Ansatz aus? Lass uns die Seiten wechseln.

Remix: Die Rückkehr der Vorhersehbarkeit
Wenn du nach ein paar Wochen im Next.js-Universum mit verschachtelten Server Components und unauffindbaren Wasserfällen völlig entnervt bist, fühlt sich Remix an wie eine kühle Dusche. Oder eher wie eine Zeitreise zurück zu den Grundlagen des Internets, aber mit modernsten React-Superkräften ausgestattet.
Die Philosophie bei der großen Entscheidung Next.js vs Remix Data Fetching könnte hier unterschiedlicher nicht sein. Remix hasst es, Server-Logik und UI-Rendering in ein und derselben Funktion zu vermischen. Stattdessen gibt es eine eiserne Grenze. Die goldene Regel lautet: Bevor deine Komponente auch nur eine Sekunde ans Rendern denkt, müssen die Daten da sein.
Wie sieht unser Tech-Blog-Szenario in Remix (bzw. React Router 7) aus? Du baust keine wilden async/await-Konstrukte in deine React-Komponente. Du exportierst eine ganz klassische, eigenständige Funktion namens loader.
1import { json } from '@remix-run/node';
2import { useLoaderData } from '@remix-run/react';
3import prisma from '@/lib/prisma';
4import { ArticleCard } from '@/components/ArticleCard';
5
6// 1. Der Loader: Läuft AUSSCHLIESSLICH auf dem Server
7export async function loader() {
8 const articles = await prisma.article.findMany({
9 orderBy: { createdAt: 'desc' },
10 take: 10,
11 });
12
13 return json({ articles });
14}
15
16// 2. Die UI-Komponente: Dumm, synchron und sauber
17export default function BlogIndex() {
18 const { articles } = useLoaderData<typeof loader>();
19
20 return (
21 <main>
22 <h1>Neueste Tech-Artikel</h1>
23 <ul>
24 {articles.map((article) => (
25 <li key={article.id}>
26 <ArticleCard title={article.title} content={article.excerpt} />
27 </li>
28 ))}
29 </ul>
30 </main>
31 );
32}Schau dir diese Eleganz an. Du hast oben den loader. Das ist reiner, purer Node.js-Code. Hier kannst du mit Passwörtern werfen, API-Keys nutzen und die Datenbank quälen. Dieser Code erreicht den Browser niemals. Remix wirft ihn beim Build-Prozess eiskalt aus dem Client-Bundle.
Unten hast du deine ganz normale, synchrone React-Komponente. Der Kleber zwischen diesen beiden Welten ist der Hook useLoaderData. Und das Beste daran? Die Typensicherheit ist absolut magisch. Ändere einen Feldnamen in der Datenbankabfrage im Loader, und TypeScript markiert dir den Fehler unten in der Komponente sofort rot. Ohne Code-Generierung, ohne extra Typen-Dateien. Es funktioniert einfach out-of-the-box.
Das mentale Modell hier ist so unfassbar befreiend. Wenn du einen Bug hast, weißt du sofort, wo du suchen musst. Ist die Datenbank lahm? Ab in den Loader. Ist der Button grün statt blau? Ab in die Komponente. Du musst nicht rätseln, ob eine Funktion jetzt gerade auf dem Server, dem Edge-Netzwerk oder im Browser deines Nutzers läuft.
Aber halt, stoppt mal. Was ist denn jetzt mit den Ladezeiten? Bei Next.js hatten wir doch dieses extrem coole Streaming mit der loading.tsx Datei. Wenn der Remix-Loader das Rendering der Komponente komplett blockiert, bis die Datenbank fertig ist... starrt der User dann hier nicht doch wieder auf einen weißen Bildschirm?

Okay, lass uns den Elefanten im Raum ansprechen. Wenn dein loader in Remix ein fettes await prisma.article.findMany() stehen hat und deine Datenbank gerade metaphorisch in der Mittagspause ist, passiert genau das, wovor alle Angst haben: Der Loader blockiert die komplette Antwort des Servers.
Es geht absolut kein HTML raus. Deine Time to First Byte (TTFB) schießt durch die Decke, und der User starrt auf einen gnadenlos weißen Screen. Das ist exakt das Problem, das Next.js mit seiner magischen loading.tsx-Datei so wunderschön wegabstrahiert hat.
Hat Remix hier also komplett gepennt? Sind wir beim Thema Next.js vs Remix Data Fetching an dem Punkt angekommen, wo Next.js den K.O.-Schlag austeilt?
Nö. Absolut nicht. Remix kontert mit einem Konzept, das sich defer nennt. Und es zeigt perfekt, wie unterschiedlich die Denkweisen der beiden Frameworks sind.
Anstatt die Lade-Magie unsichtbar ins Dateisystem zu verlagern, lässt Remix dich die volle Kontrolle behalten. Du bist der Chef im Ring. Stell dir vor, du baust ein Dashboard. Die Basis-Profildaten des Users laden in 10 Millisekunden. Aber die fetten, berechneten Analytics-Graphen brauchen drei Sekunden. Warum also auf die Graphen warten, um das Profil anzuzeigen?
In Remix entfernst du bei der langsamen Abfrage einfach das await. Ja, ernsthaft. Du übergibst dem Frontend die nackte Promise.
1import { defer } from '@remix-run/node';
2import { useLoaderData, Await } from '@remix-run/react';
3import { Suspense } from 'react';
4
5export async function loader() {
6 // Schnell: Wir warten hier (blockierend)
7 const user = await prisma.user.findFirst();
8
9 // Langsam: KEIN await! Wir reichen die Promise durch.
10 const slowAnalyticsPromise = prisma.analytics.getMassiveData();
11
12 return defer({
13 user,
14 analytics: slowAnalyticsPromise
15 });
16}
17
18export default function Dashboard() {
19 const { user, analytics } = useLoaderData<typeof loader>();
20
21 return (
22 <main>
23 <h1>Hallo {user.name}</h1>
24
25 <Suspense fallback={<p>Lade fette Graphen...</p>}>
26 <Await resolve={analytics}>
27 {(resolvedAnalytics) => <Graphs data={resolvedAnalytics} />}
28 </Await>
29 </Suspense>
30 </main>
31 );
32}Das ist echtes HTML-Streaming in Reinkultur. Remix schickt den Rahmen der Seite und das Profil sofort an den Browser. Die Seite ist da. Der User liest sein Profil. Die langsame slowAnalyticsPromise rödelt derweil auf dem Server weiter und reist quasi Huckepack auf derselben offenen Netzwerkverbindung hinterher, sobald sie fertig ist. Reacts Suspense tauscht dann im Browser den Platzhalter gegen die echten Graphen aus.
Der krasse Unterschied zu Next.js? Es ist kein schwarzer Kasten.
Bei Next.js legst du eine loading.tsx an, und das Framework wickelt im Hintergrund unsichtbar Suspense-Boundaries um deine Routen. Wenn du Pech hast und Komponenten tief verschachtelst, verlierst du völlig aus den Augen, was da eigentlich gerade auf wen wartet.
In Remix siehst du direkt in deiner Komponente, welches spezifische Stück UI auf welche exakte Daten-Promise wartet. Das ist roher, ehrlicher React-Code. Wenn es knallt, weißt du in Sekunden, in welcher Zeile der Fehler liegt.
Apropos knallen... Was passiert eigentlich, wenn die Datenbank komplett abraucht? Wie gehen wir mit Fehlern um? Das ist nämlich der nächste Punkt, an dem Server Components und Loaders brutal aufeinanderprallen.

Okay, reden wir über den Elefanten im Raum, den wir in Tutorials gerne ignorieren: Die reale Welt. Server rauchen ab. APIs werfen dir ein "429 Too Many Requests" an den Kopf. Die Postgres-Datenbank verabschiedet sich ins Wochenende. Fehler passieren.
Wenn dein fetter await prisma.article.findMany() Call in die Hose geht, willst du nicht, dass der ganze Bildschirm des Nutzers weiß wird oder eine eklige Server-Fehlermeldung (Stacktrace) im Browser landet. Wie fangen die beiden Frameworks das ab?
Beim Thema Next.js vs Remix Data Fetching sehen wir hier zwei völlig unterschiedliche Philosophien: Dateisystem-Magie vs. Modul-Kohäsion.
Next.js: Die error.tsx Konvention
In Next.js schmeißt du den Fehler im Server, aber du fängst ihn im Client. Das Framework nutzt dafür wieder seine Paradedisziplin: Das Dateisystem. Wenn deine page.tsx knallt, sucht Next.js im selben Ordner nach einer error.tsx.
So sieht diese Datei aus:
1// app/blog/error.tsx
2'use client'; // Muss eine Client Component sein!
3
4import { useEffect } from 'react';
5
6export default function ErrorBoundary({ error, reset }: {
7 error: Error & { digest?: string };
8 reset: () => void;
9}) {
10 useEffect(() => {
11 // Hier schickst du den Fehler an Sentry oder Datadog
12 console.error(error);
13 }, [error]);
14
15 return (
16 <div className="p-4 bg-red-900 text-white rounded">
17 <h2>Verdammt, die Datenbank streikt!</h2>
18 <button onClick={() => reset()}>Nochmal versuchen</button>
19 </div>
20 );
21}Das ist im Grunde eine klassische React Error Boundary, die Next.js automatisch um deine Route wickelt. Wichtig hierbei: Diese Komponente MUSS ein "use client" Tag haben. Logisch, denn sie rendert einen Button mit einem onClick-Handler, damit der Nutzer den Fetch-Vorgang (reset()) neu anstoßen kann.
Das System ist extrem mächtig. Knallt eine Komponente, stürzt nur dieser winzige Teil der Seite ab. Die Sidebar und der Header bleiben unberührt. Aber du hast eben wieder eine Datei mehr in deinem Ordner rumfliegen. Und manchmal fragt man sich bei tief verschachtelten Layouts: "Moment, welche error.tsx fängt diesen Fehler jetzt eigentlich ab?"
Remix: Alles unter einem Dach
Remix hasst verteilte Dateien für eng zusammenhängende Logik. Erinnere dich: Dein Loader und deine UI-Komponente leben bereits in derselben Route-Datei. Warum also den Error-Handler auslagern?
In Remix exportierst du einfach eine zusätzliche Funktion namens ErrorBoundary direkt in deiner Route.
1// app/routes/blog._index.tsx
2import { json } from '@remix-run/node';
3import { useLoaderData, useRouteError, isRouteErrorResponse } from '@remix-run/react';
4import prisma from '@/lib/prisma';
5
6export async function loader() {
7 const articles = await prisma.article.findMany();
8 if (!articles) {
9 throw new Response("Keine Artikel gefunden", { status: 404 });
10 }
11 return json({ articles });
12}
13
14export default function BlogIndex() {
15 const { articles } = useLoaderData<typeof loader>();
16 return <div>{/* Render articles... */}</div>;
17}
18
19// Die ErrorBoundary im exakt selben File!
20export function ErrorBoundary() {
21 const error = useRouteError();
22
23 if (isRouteErrorResponse(error)) {
24 return (
25 <div className="p-4 bg-yellow-900 text-white">
26 <h2>Fehler {error.status}</h2>
27 <p>{error.data}</p>
28 </div>
29 );
30 }
31
32 return (
33 <div className="p-4 bg-red-900 text-white">
34 <h2>Ein unerwarteter Fehler ist aufgetreten!</h2>
35 <p>{error instanceof Error ? error.message : "Unbekannt"}</p>
36 </div>
37 );
38}Das ist der absolute Wahnsinn für die Developer Experience (DX). Du hast das Problem (den fehlgeschlagenen Loader), die normale UI und die Fehler-UI gebündelt in einer einzigen Datei vor dir. Kein Tab-Wechseln im Editor. Du weißt sofort: Wenn DIESER Loader knallt, wird DIESE Boundary gerendert.
Zudem kannst du in Remix ganz bewusst im Loader einen Status-Code werfen (z. B. throw new Response(...)), und die ErrorBoundary fängt ihn extrem elegant über isRouteErrorResponse ab. Das macht das Handling von 404 (Not Found) oder 401 (Unauthorized) unfassbar simpel, ohne dass du extra "Not Found"-Seiten anlegen musst wie in Next.js (not-found.tsx).
Daten lesen ist das eine. Aber was passiert eigentlich, wenn wir Daten schreiben wollen? Wie speichern wir Formulardaten in der Datenbank? Wenn du dachtest, beim Lesen gibt es krasse Unterschiede, dann schnall dich für Server Actions vs. Remix Actions an.

Daten mutieren: Der "Speichern"-Button der Wahrheit
Daten aus einer Datenbank zu lesen, ist eigentlich der einfache Teil. Die wahre Hölle der Webentwicklung beginnt immer exakt in der Millisekunde, in der der User auf "Speichern" drückt.
Formulare. Validierung. Ladezustände. Redirects. In einer klassischen React-SPA (Single Page Application) hast du dafür einen endlosen Haufen Boilerplate geschrieben: onSubmit abfangen, e.preventDefault() aufrufen, den State auslesen, einen fetten JSON-String basteln, fetch() an eine API-Route schicken, die Antwort parsen und dann das UI updaten. Uff.
Beide Frameworks, Next.js und Remix, haben diesen Prozess radikal vereinfacht. Aber beim Thema Next.js vs Remix Data Fetching (bzw. Data Mutation) gehen sie komplett unterschiedliche Wege. Next.js nutzt RPC-Magie (Remote Procedure Calls). Remix nutzt – halte dich fest – den verdammten Browser-Standard.
Lass uns ein simples Formular bauen, um einen neuen Tech-Artikel in unsere Postgres-Datenbank zu werfen.
Next.js: Die "Server Actions" Magie
In Next.js schreibst du eine Funktion, schreibst "use server" rein und gibst sie dem Formular. Fertig. Kein API-Endpunkt nötig. Das Framework kümmert sich unter der Haube um die Erstellung eines unsichtbaren HTTP-Endpunkts.
1// app/blog/new/page.tsx
2import prisma from '@/lib/prisma';
3import { revalidatePath } from 'next/cache';
4import { redirect } from 'next/navigation';
5
6export default function NewArticle() {
7 // Das hier ist eine Server Action
8 async function createPost(formData: FormData) {
9 'use server'; // Magie an!
10
11 const title = formData.get('title') as string;
12 const excerpt = formData.get('excerpt') as string;
13
14 await prisma.article.create({
15 data: { title, excerpt }
16 });
17
18 revalidatePath('/'); // Cache löschen
19 redirect('/'); // Ab nach Hause
20 }
21
22 return (
23 <form action={createPost}>
24 <input name="title" placeholder="Titel" />
25 <textarea name="excerpt" placeholder="Inhalt" />
26 <button type="submit">Speichern</button>
27 </form>
28 );
29}Das fühlt sich beim Tippen einfach illegal an. Du übergibst eine asynchrone Server-Funktion, die vollen Datenbankzugriff hat, direkt an das action-Attribut eines HTML-Formulars. Next.js baut daraus heimlich einen POST-Request.
Das ist unfassbar bequem für uns Entwickler. Du musst keine Routen definieren. Aber es hat einen Haken: Wenn du Validierungs-Fehler (z.B. "Titel ist zu kurz") zurück ans UI schicken willst, wird es plötzlich ziemlich komplex mit Hooks wie useActionState und useFormStatus.
Remix: Oldschool HTML mit Superkräften
Remix schaut sich das an und sagt: "Warum erfinden wir das Rad neu? HTML-Formulare können seit 1995 POST-Requests absetzen."
In Remix nutzt du eine klassische <Form>-Komponente. Und anstatt den Code inline in die UI zu klatschen, exportierst du eine strikt getrennte Funktion namens action. Das ist das exakte Gegenstück zu unserem loader von vorhin. Der Loader liest Daten, die Action schreibt Daten.
1// app/routes/blog.new.tsx
2import { redirect } from '@remix-run/node';
3import { Form } from '@remix-run/react';
4import prisma from '@/lib/prisma';
5
6// 1. Die Action: Läuft nur auf dem Server
7export async function action({ request }: { request: Request }) {
8 const formData = await request.formData();
9 const title = formData.get('title') as string;
10 const excerpt = formData.get('excerpt') as string;
11
12 // Optionale Validierung hier...
13 if (title.length < 3) {
14 return { error: "Titel zu kurz!" };
15 }
16
17 await prisma.article.create({
18 data: { title, excerpt }
19 });
20
21 return redirect('/');
22}
23
24// 2. Das Formular (dumm und simpel)
25export default function NewArticle() {
26 return (
27 <Form method="post">
28 <input name="title" placeholder="Titel" />
29 <textarea name="excerpt" placeholder="Inhalt" />
30 <button type="submit">Speichern</button>
31 </Form>
32 );
33}Das ist Web-Standard in seiner reinsten Form. Du definierst method="post". Der Browser weiß exakt, was zu tun ist. Fällt das JavaScript auf dem Handy des Nutzers komplett aus (weil er im Tunnel sitzt), funktioniert das Remix-Formular trotzdem! Der Browser schickt einen nativen Request ab.
Und Fehler-Handling? Du gibst in der action einfach ein JSON-Objekt zurück, das du dir mit useActionData() in der Komponente abholst. Keine kryptischen Hooks nötig.
Aber wir haben da einen riesigen Elefanten im Raum ausgelassen. Hast du im Next.js-Code dieses kleine revalidatePath('/') bemerkt? Sobald wir Daten speichern, müssen wir dem System sagen, dass die alten Daten im Cache veraltet sind. Und genau bei diesem Thema scheiden sich die Geister endgültig.

Der Endboss: Caching und Revalidierung
Okay, tief durchatmen. Wir sind beim absoluten Endboss der Webentwicklung angekommen. Caching. Phil Karlton hat mal gesagt: "Es gibt nur zwei harte Probleme in der Informatik: Cache-Invalidierung und Dinge benennen." Er hatte verdammt recht. Wenn du Daten liest und schreibst, musst du dem Browser irgendwie mitteilen, wann die alten Daten Müll sind und er neue holen soll. Beim Thema Next.js vs Remix Data Fetching prallen hier wirklich Welten aufeinander. Es ist der Grund, warum sich Teams in Meetings fast an die Gurgel gehen.
Next.js: Granulare Kontrolle (und ein bisschen Schmerz)
Wenn du im App Router einen neuen Artikel in die Datenbank schreibst, weiß die Startseite (die den Feed anzeigt) das erstmal nicht. Sie zeigt stur die alten Daten.
Um das zu fixen, hat Next.js Werkzeuge wie revalidatePath('/blog') oder revalidateTag('articles') eingebaut. Du schreibst das in deine Server Action, drückst auf Speichern, und zack – Next.js löscht den Cache für diese spezifische Route und rendert sie neu. Das ist extrem mächtig. Du kannst chirurgisch präzise einzelne kleine Daten-Fetzen in deiner App aktualisieren, während der Rest aus dem blitzschnellen Cache kommt.
Aber ganz unter uns? Das mentale Modell ist heftig. Du musst als Entwickler permanent mitdenken: "Habe ich diese Komponente gecached? Welcher Tag hängt da dran? Revalidiere ich den richtigen Pfad?" Wenn du das an einer Stelle vergisst, sehen deine User veraltete Daten (Stale Data). Es hat einen Grund, warum Vercel in Next.js 15 und 16 das extrem aggressive Standard-Caching ("Cache everything by default") wieder ein Stück zurückgeschraubt hat. Es hat einfach zu viele Leute in den Wahnsinn getrieben.
Remix: Der "Es funktioniert einfach"-Weg
Remix schaut sich das Next.js-Caching an und schüttelt nur den Kopf. Die Architektur von Remix basiert auf einer genial einfachen Regel: Nach jeder Mutation (Action) werden automatisch alle aktiven Loaders auf der Seite neu geladen.
Das musst du dir auf der Zunge zergehen lassen. Du sendest ein Formular ab, die Action speichert den Artikel in der Postgres-Datenbank. Und dann? Nichts. Du machst gar nichts. Remix weiß: "Aha, der User hat gerade Daten verändert. Wahrscheinlich ist die UI jetzt veraltet." Das Framework triggert völlig automatisch die Loaders für deine Navigation, deinen User-Avatar und deinen Artikel-Feed neu.
Mit dem neuen "Single Fetch" Feature in React Router 7 (was quasi Remix unter der Haube ist), passiert das sogar in einem einzigen, hochoptimierten Netzwerk-Request. Keine 50 einzelnen Aufrufe mehr.
Du hast nie veraltete Daten auf dem Schirm. Niemals. Wenn du willst, dass eine Route länger lebt, nutzt du einfach stinknormale HTTP Cache-Control Header in deinem Loader. Cache-Control: max-age=3600. Der Browser versteht das. Das CDN versteht das. Du musst keine framework-spezifischen Cache-Tags auswendig lernen.
Das Gefühl, ein komplexes Dashboard in Remix zu bauen und sich nie Gedanken um veraltete UI-States machen zu müssen, ist für Frontend-Devs wie Urlaub.

Wer gewinnt den Code-Vergleich?
Wir haben den ganzen Weg vom simplen Lesen bis zum komplexen Mutieren und Cachen durchgemacht. Welches Tool nimmst du 2026 also für dein Projekt?
Wähle Next.js (App Router), wenn: Du eine Plattform baust, bei der die Grenzen zwischen extrem statischem Content und kleinen dynamischen Teilen stark verschwimmen (wie bei riesigen E-Commerce-Shops). Wenn du absolut verrückte, chirurgische Kontrolle über Caching brauchst und das Vercel-Ökosystem liebst. Du musst aber bereit sein, die steile Lernkurve der Server Components und das komplexe Cache-System zu meistern.
Wähle Remix (React Router 7), wenn: Du eine Applikation baust, die sich anfühlt wie echte Software. Dashboards, SaaS-Tools, Portale hinter einem Login. Dinge, wo User ständig wild herumklicken und Daten verändern. Remix zwingt dich, saubere Webstandards zu lernen. Loaders und Actions sind so vorhersehbar, verlässlich und stressfrei zu debuggen, dass du freitags pünktlich ins Wochenende gehst.
Am Ende des Tages sind beides unfassbar starke Werkzeuge. Die Frage ist nur: Bist du eher der Typ für das vollautomatische Smart-Home (Next.js) oder liebst du den sauberen, ehrlichen Maschinenraum (Remix)?
Teil der Serie
Next.js vs. Remix vs. Astro
Häufig gestellte Fragen (FAQ)
Zusammenfassung
Damit haben wir den Code-Vergleich tief im Maschinenraum abgeschlossen. Du kennst jetzt die genauen Architektur-Entscheidungen hinter dem Fetching. Im nächsten Cluster-Artikel schauen wir uns etwas an, das vielen Entwicklern bevorsteht: Der harte Cut – Von Vanilla React (CRA) zum modernen Framework. Wann lohnt sich die schmerzhafte Migration?

Dietrich Bojko
Senior Webentwickler
Webinteger arbeitet seit vielen Jahren produktiv mit
Linux-basierten Entwicklungsumgebungen unter Windows.
Der Fokus liegt auf
performanten Setups mit WSL 2, Docker, PHP, Node.js und modernen
Build-Tools in realen Projekten –
nicht auf theoretischen Beispielkonfigurationen.
Die Artikel dieser Serie entstehen direkt aus dem täglichen Einsatz in Kunden- und Eigenprojekten und dokumentieren bewusst auch typische Fehler, Engpässe und bewährte Workarounds.


