Startseite / Artikel / Jenseits von CRUD: Zehn Architekturgewohnheiten, die MERN-Apps wartbar halten

Jenseits von CRUD: Zehn Architekturgewohnheiten, die MERN-Apps wartbar halten

Erfahren Sie die systembezogenen Gewohnheiten, die eine MERN-Anwendung im Laufe ihres Wachstums gesund halten: Datenbesitz, API-Verträge, abgeleiteter Zustand, schichtweiser Code, kompakte Payloads und konsistente Fehlermeldungen.

1832 Wörter

Die meisten MERN-Tutorials enden mit einer funktionsfähigen CRUD-App: MongoDB speichert die Daten, Express stellt einige Routen bereit, React rendernt eine Liste – und vielleicht gibt es auch ein Anmeldeformular. Diese App läuft zwar, bleibt aber selten unverändert, wenn sie wächst. Mit zunehmenden Funktionen und Teammitgliedern werden APIs schwer zu ändern, der Zustand gerät aus dem Gleichgewicht, die Seiten werden langsamer und das Debuggen nimmt mehr Zeit in Anspruch als das Entwickeln. Die Tools sind selten die Ursache. Dieser Leitfaden behandelt zehn architektonische Gewohnheiten, die diese Probleme lösen, damit Sie eine MERN-Anwendung als ein System und nicht als vier separate Bibliotheken betrachten können.

Betrachten Sie MERN als Datenfluss, nicht als Liste von Tools

Die übliche Beschreibung des Tech-Stacks beschränkt sich meist auf seine Bestandteile:

MongoDB + Express + React + Node.js

Das ist zwar korrekt, sagt aber nichts darüber aus, wie die Komponenten miteinander zusammenarbeiten – genauso wie eine Beschreibung eines Autos als Motor, vier Räder und Lenkrad. Ein nützlicheres Bild zeigt den Datenfluss vom Benutzer zur Datenbank und zurück:

User
   │
   ▼
React
   │
HTTP
   │
   ▼
Express + Node
   │
Database Queries
   │
   ▼
MongoDB

Jeder Pfeil stellt eine Grenze mit eigenen Regeln dar: Was der Browser senden darf, was der Server akzeptiert und was in der Datenbank gespeichert wird. Die meisten unten genannten Richtlinien beziehen sich darauf, zu entscheiden, was an diesen Grenzen geschieht.

1. Geben Sie jedem Datenelement einen Verantwortlichen

Beim Entwickeln einer neuen Funktion sollte man zunächst herausfinden, wer für die betreffenden Daten verantwortlich ist. In vielen jungen Codebasen ist diese Antwort unklar. Die Details des aktuellen Benutzers können gleichzeitig im Zustand einer Komponente, in einem Redux-Store, in localStorage, in einer frischen API-Antwort oder in einem anderen Cache gespeichert sein. Früher oder später hinkt eine Kopie den anderen hinterher, wodurch die Benutzeroberfläche zwei widersprüchliche Versionen derselben Informationen anzeigt.

Eine klarere Aufteilung der Verantwortlichkeiten:

  • MongoDB ist die Quelle der Wahrheit für persistierte Daten.
  • Der Backend-Bereich ist für die Geschäftsregeln verantwortlich, die bestimmen, wie sich diese Daten ändern dürfen.
  • Die Frontend-Anwendung zeigt die Daten an und beantragt Änderungen über die API; jede Kopie auf der Client-Seite ist lediglich ein Cache und keine autoritative Quelle.

Das Leitprinzip ist, dass jede Dateneinheit genau eine einzige Quelle der Wahrheit hat und jede andere Kopie weiß, dass sie möglicherweise veraltet sein könnte. Server-Zustandsbibliotheken dienen hauptsächlich dazu, dieses Caching explizit zu verwalten; zu diesem Aspekt des Problems siehe Reconsideration des Server-Zustands mit React Query und Redux.

2. APIs als Verträge gestalten

Ein typischer erster Endpunkt sieht so aus:

app.get("/users", async (req, res) => {
  const users = await User.find();
  res.json(users);
});

Er funktioniert zwar, verspricht aber auch stillschweigend, dass die Antwort stets ein Array vollständiger Benutzerdokumente sein wird – unabhängig davon, welche Felder das Modell tatsächlich enthält. Sobald eine Mobile-App, ein Dashboard, eine Partnerintegration oder ein anderes Team von dieser Struktur abhängt, kann deren Änderung zu Fehlern führen.

Vor dem Hinzufügen eines Endpunkts entscheiden Sie sich zunächst dafür:

  • genau welche Felder zurückgegeben werden, anstatt das Rohmodell auszugeben (was auch interne oder sensible Felder preisgeben kann);
  • ob man es später ohne Beeinträchtigung der Clients ändern kann oder ob Versionierung erforderlich ist;
  • welche anderen Systeme es wahrscheinlich nutzen werden.

Eine sorgfältig entworfene API kann länger bestehen als mehrere Frontends; eine nachlässig gestaltete wird bereits innerhalb weniger Monate zu technischem Schuldenberg.

3. So wenig React-State wie möglich speichern

React wird als UI-Bibliothek vorgestellt, doch in echten Anwendungen liegt das größte Problem im State. Ein häufiger Fehler besteht darin, Werte im State zu speichern, die eigentlich berechnet werden könnten:

const [users, setUsers] = useState([]);
const [filteredUsers, setFilteredUsers] = useState([]);

Hier wird filteredUsers vollständig durch users bestimmt. Wenn man es separat speichert, muss bei jeder Aktualisierung beides synchron gehalten werden – ein Vergessen führt zu einer veralteten Liste. Berechne es lieber während des Renderns:

const filteredUsers = users.filter(user => user.active);

Die Regel besagt, dass nur das gespeichert werden soll, was nicht berechnet werden kann, und alles Weitere abgeleitet werden muss. Wenn eine Ableitung wirklich aufwändig wird, kann useMemo sie cachen, bleibt aber weiterhin abgeleitete Daten und keine zweite Quelle der Wahrheit.

4. Denken Sie daran, dass CRUD der einfachere Teil ist

Viele Projekte beschränken sich auf die vier grundlegenden Operationen:

Create
Read
Update
Delete

Ein Produktiv-Backend umfasst diese Operationen durch viel mehr: Validierung, Authentifizierung, Autorisierung, Geschäftsregeln, Rate Limiting, Audit-Trail, Logging und Benachrichtigungen. Vergleichen Sie eine naive Erstellung:

await User.create(req.body);

mit einer Version, die zuerst die Eingabe überprüft

if (!isValid(req.body))
    throw new Error("Invalid input");

und vor dem Schreiben bestätigt, ob der Aufrufer dazu berechtigt ist:

if (!canCreateUser(req.user))
    throw new Error("Unauthorized");await User.create(req.body);

Die naive Version birgt außerdem das Risiko der Massenzuweisung: Wenn req.body direkt an create übergeben wird, kann ein Client jedes Feld setzen, das vom Schema akzeptiert wird – einschließlich eines role-Flags. Validieren und ausschließlich die erlaubten Felder auswählen. Beachten Sie außerdem, dass ein fehlgeschlagener Berechtigungscheck konzeptionell als 403 (verboten) gilt und nicht als 401, unabhängig von der Fehlermeldung. Daten zu schreiben ist einfach; sie zu schützen, macht die Backend-Entwicklung schwierig.

5. Trennen Sie die Aufgaben in Schichten auf

Der deutlichste Unterschied zwischen einer Hobby-Codebasis und einer professionellen besteht darin, wo die Logik untergebracht ist. Die Vermischung von Aufgaben führt dazu, dass Route-Handler voller Datenbankabfragen, React-Komponenten voller Validierungsregeln sowie Controller voller Geschäftslogik entstehen, wobei jede dieser Dateien ständig größer wird.

In einer schichtbasierten Backend-Architektur wird jeder Schicht eine bestimmte Aufgabe zugewiesen:

Routes
   │
Controllers
   │
Services
   │
Repositories
   │
Database

Routen verknüpfen URLs mit Handlern, Controller wandeln HTTP-Anfragen in Funktionsaufrufe um, Services enthalten Geschäftsregeln, Repositorien kommunizieren mit der Datenbank. Kleine, einzigartige Einheiten sind einfacher zu testen und zu ändern. Für eine ausführlichere Erklärung siehe das schichtbasierte Node.js API-Design.

6. Beheben Sie zuerst die Leistungsprobleme an der Datenquelle

Wenn gefragt wird, wie man eine React-Anwendung beschleunigen kann, greifen die meisten Entwickler zu useMemo, React.memo und useCallback. Das hilft zwar, doch viele Leistungsprobleme entstehen bereits vor dem Eingreifen von React. Stellen Sie sich eine solche Anfrage vor:

GET /users

das zurückgibt

50,000 users

während der Bildschirm nur anzeigt

10 users

Auch die größte Menge an Memoisierung kann nicht ausgleichen, dass Zehntausende unnötiger Datensätze übertragen und verarbeitet werden müssen. Gegensteuern Sie das bereits in der Quelle:

  • Seitern Sie die Ergebnisse;
  • Filtern Sie auf dem Server;
  • Übergeben Sie nur die Felder, die der Client benötigt;
  • Komprimieren Sie die Antworten;
  • Cachen Sie gezielt und mit einem klaren Ablaufplan für die Invalidation.

Der schnellste Komponente zur Darstellung ist eine, die niemals Daten erhält, die sie nicht benötigt.

7. Machen Sie Fehlerbehandlung zum Bestandteil des Designs

Entwicklungscode schluckt oft solche Fehler einfach:

try {
   ...
}
catch(error){
   console.log(error);
}

Das Protokollieren und Weitermachen verdeckt den Fehler vor dem Client sowie bei der Überwachung. Produktions-APIs benötigen Fehler, die konsistent und maschinenlesbar sind:

return res.status(400).json({
    message: "Invalid email address",
    code: "INVALID_EMAIL"
});

Eine stabile Struktur mit einer für Menschen lesbaren message-Angabe sowie einem für Maschinen lesbaren code bedeutet, dass die Frontend-Anwendung Codes auf spezifische Benutzeroberflächenmeldungen zuordnen kann, Protokolle nach Code gruppiert werden können, Warnungen bei ungewöhnlichen Häufigkeiten ausgelöst werden können und das Debuggen von einer bekannten Kategorie statt von einem Stacktrace ausgeht. Fehler sind unvermeidlich; das Ziel ist es, sie vorhersehbar zu handhaben.

8. Code nach Funktionalitäten organisieren

Mit zwanzig Dateien funktioniert jede Ordnerstruktur. Bei fünfhundert Dateien spielt die Struktur eine große Rolle. Eine nach technischem Typ gruppierte Struktur verteilt eine Funktionalität über den gesamten Ordnerbaum:

routes/
controllers/
models/

Durch Gruppierung nach Funktionalitäten bleibt alles, was zu einem bestimmten Bereich gehört, an einem Ort:

users/
    routes.js
    controller.js
    service.js
    validation.js
orders/
    routes.js
    controller.js
    service.js

Wenn sich die Logik der Bestellungen ändert, öffnet man einfach den Ordner orders und nichts anderes. Funktionsordner erleichtern zudem die Zuweisung der Verantwortlichkeiten, die Code-Review-Prozesse sowie eine spätere Aufteilung in separate Dienste erheblich.

9. Denken Sie in Systemen, nicht in Tickets

Eine Funktionsanfrage wie „Anmeldung hinzufügen“ kann eng gefasst beantwortet werden – mit einem Formular und einer Route. Ein systemorientierter Ansatz stellt die umliegenden Fragen: Wie funktioniert die Authentifizierung von Anfang bis Ende, wo werden Tokens gespeichert, wie werden Berechtigungen durchgesetzt, was passiert, wenn ein Token abläuft, und wie wird sich ein zukünftiger mobiler Client anmelden? Die frühzeitige Beantwortung dieser Fragen kostet zwar heute etwas mehr Zeit, vermeidet aber spätere Überarbeiten.

10. Wählen Sie Kompromisse bewusst

Keine Architektur ist in jeder Situation die beste. Jede Option bringt Vorteile mit sich, verursacht aber auch Nachteile:

  • Einfache Architektur: schneller zu entwickeln, schwieriger skalierbar.
  • Mikroservices: unabhängige Skalierung, deutlich höhere Betriebskomplexität.
  • Globales State: einfaches Teilen zwischen Komponenten, schwierigere Fehlersuche.
  • Normalisiertes Datenbankdesign: weniger Duplikate, mehr Join-Operationen oder Abfragen.
  • Aggressives Caching: schnellere Antworten, das anhaltende Problem der Cache-Invalidierung.

Fähige Ingenieure sind nicht diejenigen, die jedes Muster kennen, sondern diejenigen, die erklären können, wann sich das Einsatz eines bestimmten Musters lohnt.

Wie wachsende MERN-Projekte typischerweise verfallen

Phase 1: Alles ist einfach

Die erste Veröffentlichung umfasst das Wesentliche:

CRUD
Authentication
Dashboard
Deployment

Der Code ist klein, und jeder versteht ihn vollständig.

Phase 2: Das Wachstum bringt Abkürzungen zum Vorschein

Weitere Benutzer, Funktionen und Entwickler kommen hinzu. Doppelte Logik, inkonsistente Endpunkte, langsame Seiten, verwickelter Zustand sowie schwieriges Debuggen tauchen im gesamten Codebase auf.

Phase 3: Die Tools werden beschuldigt

Das Team kommt zu dem Schluss, dass React sich nicht skalieren lässt oder dass die Wahl von MongoDB ein Fehler war. In der Regel stimmt das nicht. Die Architektur hat einfach nie zusammen mit der Anwendung weiterentwickelt werden.

Eine Restaurant-Analogie für die Schichten

Betrachten Sie den Stack als ein Restaurant. MongoDB ist die Vorratskammer, in der alle Zutaten aufbewahrt werden. Express und Node sind die Küche: Sie entscheiden, was zubereitet wird, wie es zubereitet wird und wer bestellen darf. React ist die Empfangszone, die den Gästen die fertigen Gerichte präsentiert. Die Gäste müssen nicht wissen, wie die Küche funktioniert, und es ist der Küche egal, wie die Teller auf dem Tisch angeordnet sind. Jeder Bestandteil erledigt seine Aufgabe gut – genau diese Trennung benötigt eine MERN-Anwendung.

Wichtige Erkenntnisse

Das Verständnis von MERN dreht sich weniger um das Schreiben von Abfragen, Routen und Komponenten, sondern vielmehr darum, den Datenpfad zu erkennen, Geschäftsregeln in die richtige Schicht zu platzieren, APIs weiterzuentwickeln, ohne die Clients zu stören, den Zustand minimal zu halten und zu erkennen, wie sich frühe Entscheidungen auswirken.

  • Geben Sie jedem Datenstück einen Eigentümer und betrachten Sie jede andere Kopie als Cache.
  • Betrachten Sie Endpunkte als Verträge und geben Sie explizite, gezielte Strukturen zurück.
  • Ermitteln Sie den Zustand anstelle dessen, ihn zu duplizieren.
  • Überprüfen, autorisieren und listen Sie Felder vor dem Schreiben von Inhalten in einer Whitelist ein.
  • Lassen Sie die Payloads am Quellcode klein bleiben, bevor Sie die Darstellung optimieren.
  • Geben Sie konsistente, kodierte Fehler zurück und gruppieren Sie den Code nach Funktionalitäten.
  • Wenn eine neue Funktion entsteht, ist die nützlichste Frage nicht, wie man sie baut, sondern wohin jede Verantwortung gehört.

    Verwandte Artikel