
Backend-Dienst
Ein E-Commerce-Backend ohne Oberfläche: 15 Routengruppen, 18 Prisma-Modelle, Identität über JWT und Refresh-Token, Anbieter-Login, Rate-Limiting, ein Prüfprotokoll und ein Metrik-Endpunkt. Der Vertrag entsteht aus Swagger, der Dienst wird mit Docker paketiert.
In einer Anwendung mit Oberfläche ist der Bildschirm die Antwort auf "läuft es". In einem Dienst ohne Oberfläche gibt es diese Antwort nicht: Der Dienst kann laufen und sich falsch verhalten, und nur die konsumierende Seite bemerkt es — meist zu spät. Das eigentliche Problem ist hier also nicht die Fachlogik, sondern Vertrag und Sichtbarkeit. Zweitens ist in einer E-Commerce-API eine wiederholte Anfrage der Normalfall: Die Verbindung bricht ab, der Client versucht es erneut, der Zahlungsanbieter schickt dieselbe Benachrichtigung ein zweites Mal. Wird "was passiert bei doppelter Verarbeitung" an jedem Endpunkt einzeln beantwortet, wird irgendwann einer vergessen.
Zwei Arten von Konsumenten. Der direkte Client — eine Web- oder mobile Oberfläche: liest den Katalog, schreibt Warenkörbe und Bestellungen, verwaltet Sitzungen. Der serverseitige Konsument — ein Zahlungs- oder Versandanbieter: schickt Webhooks, weist sich mit einem geteilten Geheimnis aus und kann dieselbe Benachrichtigung wiederholen. Es gibt eine dritte Partei: die betreibende Person, deren Bedarf kein Bildschirm ist, sondern Logs, Metriken und ein Prüfprotokoll.
Einzelner Entwickler. Datenmodell und Migrationen, die Middleware-Kette, Identitäts- und Berechtigungsschicht, Bestell-Statusmaschine, Prüf- und Beobachtbarkeitsschicht, der Swagger-Vertrag und das Docker-Paket.
Ein einzelner Dienst auf Express; eine Anfrage durchläuft eine feste Middleware-Kette: Sicherheits-Header, Ressourcenfreigabe-Richtlinie, Anfrage-ID, Rate-Limiting, JWT-Prüfung, Rollenprüfung, Besitzprüfung und Zod-Schemaprüfung. Am Ende der Kette führt der Controller nur die Fachlogik aus; Fehlerbehandlung und die Form des Fehlerkörpers sind in einer einzigen Fehler-Middleware gebündelt, sodass jeder Endpunkt denselben Fehlerumschlag liefert. Der Bestellstatus liegt nicht in den Controllern, sondern in einem eigenen Statusmaschinen-Modul — welcher Zustand in welchen übergehen darf, steht an einer Stelle. Wiederholte Anfragen behandelt eine eigene Idempotenz-Middleware, sodass kein Endpunkt seine eigene Lösung erfindet. Auf der Datenseite Prisma und PostgreSQL; das Schema führt für die Produktsuche eine eigene Suchvektor-Spalte, und Listen-Endpunkte nutzen cursorbasierte Paginierung statt Seitenzahlen. Die Beobachtbarkeit hat drei Teile: strukturierte Logs, Anfrageprotokollierung und ein Metrik-Endpunkt.
Eine wiederholte Anfrage ist nicht endpunktspezifisch; Hinzufügen zum Warenkorb, Anlegen einer Bestellung und Verarbeiten eines Webhooks teilen dasselbe Problem. Liegt die Lösung in der Middleware, kann kein Endpunkt sie vergessen — der Schutz hängt an der Routendefinition, nicht an der Disziplin eines Controllers.
KompromissDer Client muss einen Idempotenzschlüssel erzeugen und bei einem erneuten Versuch denselben senden; ein Client, der das nicht tut, profitiert nicht vom Schutz. Der Speicher der verarbeiteten Schlüssel wächst zudem stetig und muss aufgeräumt werden.
Der Bestell-Lebenszyklus ist die am stärksten verzweigte Stelle des Systems: Zahlung, Kommissionierung, Versand, Zustellung, Storno und Rückgabe. Sind die Übergangsregeln über Controller verstreut, wird dieselbe Regel an mehreren Stellen unterschiedlich geschrieben und läuft mit der Zeit auseinander. Sie in einem Modul zu bündeln, macht die gültigen Übergänge an einer Stelle lesbar.
KompromissDie Regel liegt in der Anwendung, nicht in der Datenbank: Ein direkt in SQL ausgeführtes Update kann die Statusmaschine umgehen. So wie ich bei Ustura die Überschneidungsregel auf eine Datenbank-Constraint reduziert habe, hätten sich auch hier Übergänge auf Datenebene erzwingen lassen; das ist nicht geschehen.
Der Access-Token ist kurzlebig und zustandslos; der Refresh-Token existiert als Datensatz in der Datenbank. Der Grund ist die Widerrufbarkeit: Ein zustandsloser Refresh-Token bleibt bis zum Ablauf gültig, und eine gestohlene Sitzung ließe sich nicht schließen. Anbieter-Logins werden über ein eigenes Kontomodell verknüpft, sodass eine Person mehrere Anmeldewege haben kann.
KompromissJede Erneuerungsanfrage bedeutet einen Datenbanklesevorgang — der Skalierungsvorteil eines zustandslosen Entwurfs wird hier bewusst aufgegeben. Auch diese Tabelle wächst, abgelaufene Datensätze müssen aufgeräumt werden.
Der Metrik-Endpunkt beschreibt den inneren Zustand des Systems, die Swagger-Oberfläche den gesamten Vertrag. Beides ist Aufklärungsmaterial für einen Angreifer. Beide wurden hinter Authentifizierung gelegt; der Webhook-Endpunkt wird zusätzlich mit einem geteilten Geheimnis geprüft.
KompromissGeschlossene Dokumentation bedeutet einen zusätzlichen Zugriffsschritt für Entwickler, die die API nutzen wollen. Auch der Schutz des Metrik-Endpunkts erfordert eine eigene Konfiguration auf der Sammler-Seite — mit einem Sammler ohne Authentifizierungsunterstützung funktioniert er nicht direkt.
Den Anfragekörper zu loggen, erleichtert die Fehlersuche, doch derselbe Körper kann Passwort, Token oder Adresse enthalten. Maskiert wird beim Schreiben und feldweise; ein sensibles Feld gelangt gar nicht erst ins Log, statt nachträglich bereinigt zu werden.
KompromissDie Liste der zu maskierenden Felder wird von Hand gepflegt; ein neues sensibles Feld muss eingetragen werden, und nichts erinnert daran. Ein maskiertes Feld ist auch beim Debuggen unsichtbar, manche Fehler lassen sich also nicht über das Log verfolgen.
Express wurde gewählt, damit die Middleware-Kette lesbar bleibt: Was eine Anfrage in welcher Reihenfolge durchläuft, lässt sich am Code verfolgen — und das heißt, Sicherheitsentscheidungen bleiben sichtbar. Prisma hält Beziehungen und Migrationshistorie eines Schemas mit 18 Modellen nachvollziehbar; die Schemadatei ist zugleich Dokumentation des Datenmodells. Zod bündelt die Prüfung des Anfragekörpers in einer Middleware und liefert die Typen aus demselben Schema. Der Swagger-Vertrag steht als Kommentar neben dem Code — so läuft ein separates Dokument schwerer vom Code weg. Docker macht den Dienst mitsamt seinen Abhängigkeiten zu einer lauffähigen Einheit; in einem Dienst ohne Oberfläche bedeutet "läuft auf meiner Maschine" nichts.
Die Middleware-Kette beginnt mit Sicherheits-Headern, die Ressourcenfreigabe-Richtlinie ist ausdrücklich definiert. Rate-Limiting ist je Endpunkt konfiguriert und bei Identitätsendpunkten enger. Passwörter liegen mit bcrypt gehasht vor, der Access-Token ist kurzlebig, und weil der Refresh-Token in der Datenbank steht, ist er widerrufbar. Die Berechtigung hat drei Stufen: Authentifizierung, Rollenprüfung und Besitzprüfung — selbst eine Person mit der richtigen Rolle kann fremde Datensätze nicht bearbeiten. Der Webhook-Endpunkt wird mit einem geteilten Geheimnis geprüft. Datei-Uploads laufen über eine eigene Middleware. Metrik- und Swagger-Endpunkte liegen hinter Authentifizierung. Änderungen werden in ein eigenes Prüfprotokoll-Modell geschrieben; Log-Felder werden beim Schreiben maskiert.
Dies ist ein Backend-Dienst; eine Seitengeschwindigkeitsmessung ist bedeutungslos, und da keine gespeicherte Lasttest-Ausgabe vorliegt, steht hier keine Zahl. Strukturell gibt es drei Entscheidungen: Listen-Endpunkte nutzen cursorbasierte Paginierung, sodass die Abfragekosten auch bei tiefen Seiten konstant bleiben; für die Produktsuche führt das Schema eine eigene Suchvektor-Spalte, sodass die Suche nicht bei jeder Anfrage Text durchsucht; und weil der Metrik-Endpunkt Werte wie Anfragezahl und -dauer nach außen gibt, lässt sich das Verhalten des Dienstes messen statt vermuten.
In diesem Repository gibt es keine automatischen Tests, und bei einem Backend-Dienst ist das die teuerste Lücke. Die einfachste und wertvollste Teststelle liegt auf der Hand: Die Bestell-Statusmaschine ist bereits ein reines Modul, als Ein- und Ausgabe prüfbar, und trägt die am stärksten verzweigte Regel des Systems. Heute würde ich dort beginnen und danach die Idempotenz-Middleware mit derselben, zweimal gesendeten Anfrage prüfen. Der zweite Punkt ist der Ort der Zustandsregel: Ich habe die Übergänge in der Anwendung gehalten, während bei Ustura die Reduktion der Überschneidungsregel auf eine Datenbank-Constraint den Schutz auf alle Schreibpfade zugleich anwandte. Hier kann ein direkt in SQL ausgeführtes Update die Statusmaschine umgehen; heute würde ich zumindest eine Check-Constraint gegen ungültige Zustände ergänzen.
Sagen Sie mir, was Sie bauen wollen; ich sage Ihnen vorab, wie lange es dauert und wo man anfängt.