Backend-Antipatterns: 7 kostspielige Fehler und ihre praktischen Lösungen
Erfahren Sie, wie Sie sieben häufige Fehler in der Backend-Entwicklung erkennen und beheben können – von überdimensionierten Controllern bis hin zu unverarbeiteten Fehlern – bevor sie Probleme in der Produktion verursachen.
Wenn man mit der Backend-Entwicklung beginnt, ist es verlockend anzunehmen, dass die eigentliche Herausforderung darin besteht, mehr Tools zu meistern.
Node.js.
Express.
PostgreSQL.
Redis.
Docker.
Nachrichtenwarten.
Systemdesign.
Aber nach dem Veröffentlichen mehrerer Anwendungen wird eine andere Wahrheit klar.
Das Kennen von mehr Tools macht einen nicht automatisch zu einem besseren Backend-Entwickler.
Der größte Fortschritt entsteht durch das Machen von Fehlern, das Verstehen ihrer Ursachen und das Sichern dagegen, dass sie erneut auftreten.
Hier sind sieben Fehler im Backend, aus denen man lernen kann, zusammen mit dem besseren Vorgehen.
1. Alles im Controller unterbringen
Das ist oft einer der ersten Fehler, auf die man stößt.
Ein Endpunkt könnte ursprünglich so aussehen:
app.post("/orders", async (req, res) => {
const { userId, productId, quantity } = req.body;
const user = await db.users.findUnique({
where: { id: userId }
}); if (!user) {
return res.status(404).json({
message: "User not found"
});
} const product = await db.products.findUnique({
where: { id: productId }
}); if (!product) {
return res.status(404).json({
message: "Product not found"
});
} if (product.stock < quantity) {
return res.status(400).json({
message: "Not enough stock"
});
} const order = await db.orders.create({
data: {
userId,
productId,
quantity
}
}); await sendEmail(user.email); return res.status(201).json(order);
});
Es funktioniert.
Aber schauen Sie sich an, was diese eine Funktion jetzt alles umfasst:
- Validierung
- Datenbankabfragen
- Business-Regeln
- Bestandsprüfung
- Erstellung von Bestellungen
- E-Mails
- HTTP-Antworten
Sobald ein Projekt weiter wächst, werden Funktionen mit so vielen Verantwortlichkeiten zu unübersichtlichen Logikblöcken.
Die Lösung besteht darin, die Verantwortlichkeiten aufzuteilen.
Request
↓
Controller
↓
Service
↓
Repository
↓
Databas
Die Aufgabe des Controllers ist HTTP.
Die Service-Schicht kümmert sich um die Business-Logik.
Die Repository-Schicht ist für den Datenzugriff zuständig.
Das bedeutet nicht, dass eine kleine Anwendung sechs Abstraktions-Ebenen benötigt.
Das bedeutet vielmehr, dass jeder Teil des Systems eine klar definierte Aufgabe haben sollte.
2. Dem Frontend vertrauen
Dieser Fehler kann Fehler sowie, noch schlimmer, Sicherheitslücken verursachen.
Nehmen wir an, die Frontend-Plattform sendet diesen Payload:
{
"price": 10,
"quantity": 2
}
Es ist verlockend, einfach den vom Client übermittelten Preis zu verwenden, um die Gesamtkosten der Bestellung zu berechnen.
Tun Sie das nicht.
Nichts hindert einen bösartigen Client daran, Folgendes zu senden:
{
"price": 1,
"quantity": 100
}
Die Frontend-Plattform steht unter der Kontrolle des Benutzers, nicht Ihrer.
Ihr Backend muss die tatsächlich wichtigen Regeln überprüfen und durchsetzen.
Zum Beispiel:
const product = await productRepository.findById(
productId
);
const total = product.price * quantity;
Das Backend, nicht der Client, sollte die Quelle der Wahrheit bezüglich der Preise sein.
Diese Vorsichtsmaßnahme muss auch auf weitere Bereiche ausgedehnt werden, die ein Client beeinflussen könnte:
- Benutzerrollen
- Berechtigungen
- Rabatte
- Lagerbestände
- Zahlungsbeträge
- Kontostatus
- Ressourcenbesitz
Betrachten Sie die Frontend-Plattform als Werkzeug, um die Art und Weise zu gestalten, wie Benutzer Ihr Produkt nutzen.
Es handelt sich dabei nicht um eine Sicherheitsgrenze.
3. Falsche Fehlerbehandlung
Früher sah die Fehlerbehandlung oft so aus:
try {
// something
} catch (error) {
console.log(error);
return res.status(500).json({
message: "Something went wrong"
});
}
Es ist an sich nichts Falsches daran, einen Allzweck-Fallback zu verwenden.
Das Problem liegt darin, in jeder Situation darauf zurückzugreifen.
Ein fehlendes Benutzerkonto ist nicht unbedingt ein Fehler vom Typ 500.
Eine fehlerhafte Anfrage ist nicht unbedingt ein Fehler vom Typ 500.
Ein doppelter E-Mail-Adresse ist nicht unbedingt ein Fehler vom Typ 500.
Ihr Backend muss verschiedene Fehlerarten voneinander unterscheiden können.
Zum Beispiel:
400 → Invalid request
401 → Authentication required
403 → Not allowed
404 → Resource not found
409 → Conflict
422 → Validation failure
500 → Unexpected server error
Die gewählten Statuscodes hängen von den Konventionen Ihrer API ab, doch Konsistenz ist wichtiger als das genaue Schema.
Gestrukturierte Fehlerantworten helfen ebenfalls.
Zum Beispiel:
{
"success": false,
"message": "User already exists",
"code": "USER_ALREADY_EXISTS"
}
Mit diesem Format muss die Frontend-Seite nicht raten, was schiefgelaufen ist.
4. Hardcoding der Konfiguration
Der Fehler scheint zunächst harmlos zu sein, bis man versucht, etwas zu deployen.
Etwas in der Art:
const databaseUrl =
"postgresql://user:password@localhost:5432/app";
Oder:
const jwtSecret = "my-secret";
Vermeiden Sie dieses Muster vollständig.
Jedes Umfeld, in dem Sie arbeiten, benötigt eigene Einstellungen.
Möglicherweise haben Sie:
Development
↓
localhost
Staging
↓
staging databaseProduction
↓
production database
Verlassen Sie sich stattdessen auf konfigurationsspezifische Einstellungen je nach Umfeld:
DATABASE_URL=
REDIS_URL=
JWT_SECRET=
PAYMENT_API_KEY=
EMAIL_API_KEY=
Und übergeben Sie niemals Geheimnisse in das Versionierungssystem.
Eine .env-Datei eignet sich für die lokale Entwicklung, doch Produktionsumgebungen erfordern geeignete Tools zur Verwaltung von Geheimnissen und Konfigurationen.
Das grundlegende Prinzip hier ist:
Ihr Code sollte nicht eng an umgebungsbezogene Konfigurationswerte gebunden sein.
5. Skalieren, bevor es wirklich notwendig ist
Das ist eine Falle, in die viele Entwickler irgendwann geraten, und sie lässt sich zum Zeitpunkt der Entscheidung leicht rechtfertigen.
Ein Team startet ein neues Projekt und geht sofort dazu über:
"Was passiert, wenn morgen 10 Millionen Menschen auftauchen?"
Deshalb fügen sie Folgendes hinzu:
Microservices
Kafka
Redis
Kubernetes
Multiple databases
API Gateway
Event-driven architecture
Auf dem Papier kann das System eine enorme Skalierung bewältigen.
Aber es gibt tatsächlich nur fünf Nutzer.
Das ist keine solide Architektur – das ist Komplexität, die noch niemand benötigt.
Für die meisten Projekte macht es mehr Sinn, einfach anzufangen:
Client
↓
Node.js Application
↓
PostgreSQL
Und nur neue Komponenten hinzuzufügen, wenn es einen konkreten Grund dafür gibt:
Need caching?
→ Redis
Need background jobs?
→ Queue + WorkerNeed more API capacity?
→ Multiple instances + Load BalancerDatabase becoming a bottleneck?
→ Optimize queries / indexes / architecture
Lassen Sie die Architektur gemeinsam mit den tatsächlichen, nachgewiesenen Anforderungen wachsen.
Greifen Sie nicht zu einem verteilten System, nur weil irgendein Video behauptet, das würden „echte“ Senior-Entwickler so machen.
6. Blockierung der Anfrage bei langsamer Verarbeitung
Dies kann die Nutzung einer API erheblich beeinträchtigen.
Bilden Sie sich folgende Konfiguration vor:
app.post("/order", async (req, res) => {
const order = await createOrder(); await sendEmail(); await generateInvoice(); await notifyWarehouse(); await updateAnalytics(); return res.json(order);
});
Der Benutzer wartet hier darauf, dass alle fünf Schritte nacheinander abgeschlossen werden.
Falls bereits eine externe Anfrage fünf zusätzliche Sekunden dauert, ist die gesamte Antwort nun fünf Sekunden langsamer.
Ein besseres Vorgehen besteht darin, nur die Arbeiten innerhalb der Anfrage auszuführen, die tatsächlich sofort erledigt werden müssen.
Alles Weitere kann in eine Warteschlange für den Hintergrundverarbeitung eingereiht werden.
Client
↓
API
↓
Create Order
↓
Queue Jobs
↓
Response
Danach folgt:
Queue
↓
Worker
├── Send Email
├── Generate Invoice
├── Notification
└── Analytics
Genau in solchen Szenarien zeigt sich, warum Tools wie BullMQ in Kombination mit Redis nützlich sind.
Aber es gibt hier noch eine weitere, leicht übersehene Lektion:
Hintergrundaufgaben müssen idempotent sein und Fehler angemessen handhaben.
Falls eine Aufgabe versehentlich zweimal ausgeführt wird, können folgende Risiken entstehen:
- Mehrfachabrechnung bei einem Kunden
- Aussendung doppelter Benachrichtigungen
- Einfügen doppelter Datensätze in die Datenbank
Einfaches Übertragen der Arbeit auf eine Warteschlange löst dieses Problem nicht von selbst.
Die Aufgabe selbst muss unter Berücksichtigung dieses Aspekts konzipiert werden.
7. Ohne Überblick in der Produktion
dieser Fehler bleibt in der Regel unsichtbar, bis tatsächlich etwas kaputtgeht.
Stellen Sie sich vor, eine Live-API beginnt plötzlich Fehler auszusenden.
Auf den ersten Blick scheint der Server in Ordnung zu sein.
Auch der Code wirkt einwandfrei.
Doch hier fehlt etwas:
No useful logs
No request IDs
No metrics
No error tracking
No database monitoring
Zu diesem Zeitpunkt bleibt nur noch das Raten übrig.
Das Debuggen verwandelt sich in eine Abfolge von Achselzucken:
„Vielleicht ist Redis down?“, „Vielleicht arbeitet die Datenbank extrem langsam?“, „Vielleicht gibt es Probleme beim Zahlungsdienstleister?“
Als Ingenieur ist das eine unangenehme Situation.
Zumindest sind nützliche Protokolle unerlässlich.
Zum Beispiel:
{
"level": "error",
"requestId": "req_123",
"route": "/orders",
"userId": "user_456",
"message": "Payment provider timeout"
}
Mit einer solchen Ausgabe wird es möglich, genau herauszufinden, was schiefgelaufen ist und wo.
Sobald Systeme wachsen, erweitert sich die Überwachbarkeit in der Regel auf Folgendes:
- Anwendungsprotokolle
- Fehlerverfolgung
- CPU- und Speichernutzung
- Metriken auf Datenbankebene
- API-Antwortzeiten
- Größe des Warteschlangenpuffers
- Fehler von externen APIs
- Health-Check-Endpunkte
Ein System kann nicht gesund bleiben, wenn man keine Einblicke in sein Verhalten hat.
Die wichtigere Lektion
Wenn man einen Schritt zurücktritt, lassen sich fast alle diese Probleme auf dieselbe zugrunde liegende Gewohnheit zurückführen.
Die Frage, die viele Entscheidungen leitet, lautet in der Regel:
">Funktioniert diese Funktion?"
während sie eigentlich lauten sollte:
">Ist es einfach, diese Funktion zu ändern, zu debuggen und in der Produktion auszuführen?"
Diese Veränderung der Herangehensweise verändert fast alles daran, wie Software entwickelt wird.
Was vor dem Markieren einer API als abgeschlossen überprüft werden sollte
Bevor eine Backend-Funktion als abgeschlossen markiert wird, ist es hilfreich, die folgenden Überprüfungen durchzuführen.
Code
- Hat jede Schicht eine klare, einzige Verantwortung?
- Kann die Geschäftslogik getrennt voneinander getestet werden?
- Bleiben die Controller angemessen schlank?
Sicherheit
- Wird alle eingehende Eingabe validiert?
- Werden Berechtigungsprüfungen auf Serverseite durchgeführt?
- Bleiben Geheimnisse unzugänglich?
Datenbank
- Sind die Abfragen effizient?
- Gibt es die richtigen Indizes?
- Gibt es ein verstecktes N+1-Abfrage-Problem?
Leistung
- Sind vermeidbare sequenzielle Aufrufe beseitigt?
- Wird die aufwändige Verarbeitung an einen Hintergrundprozess delegiert?
- Könnte Caching die Situation verbessern?
Toverlässigkeit
- Was ist die Notlösung, falls eine externe API versagt?
- Sind Wiederholungsversuche sinnvoll konfiguriert?
- Sind Hintergrundaufgaben sicher, mehrmals ausgeführt zu werden?
- Was passiert, wenn Redis oder die Datenbank nicht erreichbar ist?
Betrieb
- Kann man erkennen, was passiert ist, wenn etwas fehlschlägt?
- Sind die Protokolle tatsächlich nützlich?
- Gibt es Health Checks?
- Kann die Leistung einer API gemessen werden?
Ein fehlerfreies System ist nicht das Ziel.
Aber es ist unerlässlich zu wissen, was passiert, sobald etwas schiefgeht.
Die 7 Fehler auf einen Blick
| Fehler | Besseres Vorgehen |
|---|---|
| Alles in Controllern untergebracht | Verantwortlichkeiten auf verschiedene Schichten verteilen |
| Vertrauen auf Daten vom Frontend | Alles serverseitig validieren |
| Einheitliche Fehlerbehandlung für alle Fälle | Eine konsistente Fehlerstrategie anwenden |
| Hartkodierte Geheimnisse | Angemessene Verwaltung von Umgebungen und Konfigurationen |
| Zu frühes Skalieren | |
| Schwere Aufgaben, die Anfragen blockieren | Sie in Hintergrundaufgaben verschieben |
| Keine Einblicke in die Produktion |
Fazit
Es gibt die weit verbreitete Annahme, dass das Fortschreiten als Backend-Entwickler lediglich bedeutet, mehr Tools und Technologien zu sammeln.
Eine genauere Sichtweise ist, dass es eigentlich darum geht, Abwägungen zu verstehen.
Soll diese Operation synchron oder asynchron ablaufen?
Sind diese Daten zum Caching geeignet?
Braucht diese Abfrage einen Index?
Sollte dies ein eigenständiger Dienst sein?
Was ist der Plan, falls Redis ausfällt?
Was ist der Plan, falls die Datenbank extrem langsam wird?
Was ist der Plan, falls eine Aufgabe versehentlich zweimal ausgeführt wird?
Was ist der Plan, falls die genutzte externe API versagt?
Die Kenntnis der Antworten auf diese Fragen ist weitaus wichtiger als einfach nur zu wissen, wie man ein weiteres Paket installiert.
Denn Produktionsysteme werden nicht danach beurteilt, wie sie sich verhalten, wenn alles reibungslos funktioniert.
Echte Ingenieurskunst zeigt sich erst, wenn etwas schiefgeht.
Jeder heute erkannte Fehler bedeutet einen Produktionsfehler weniger, den später gelöscht werden muss.
Verwandte Artikel
- Häufige API-Vertragsfehler, die die Zuverlässigkeit des Frontends beeinträchtigen – Lernen Sie zehn häufige Mängel im Backend-API-Design – von incohenzten Antwortstrukturen bis hin zu instabiler Paginierung – die das Vertrauen des Frontends untergraben, sowie wie man sie behebt.