lightMe – Ganzheitliche Health-App
Situation
Der Markt für Fitness- und Ernährungs-Apps ist stark fragmentiert – Nutzer:innen jonglieren getrennt mit Kalorienzählern, Aktivitätstrackern, Schlaftagebüchern und kostenpflichtigen Coaching-Programmen. Für das 12-Wochen-Programm von LightMe stachen drei Kernprobleme heraus. Die Ernährungsdaten waren fragmentiert und voller Lücken: keine einzelne API deckt alle Nahrungsmittel, Barcodes und Eigenkreationen zuverlässig ab, sodass Suchanfragen fehlschlugen oder unvollständige Makronährwerte lieferten. Admins und Coaches hatten keine Möglichkeit, den Fortschritt der Nutzer:innen (z. B. prozentualer Gewichtsverlust, Einhaltung der Vorgaben) kohortenbasiert auszuwerten. Und die Zahlungsströme über die Stufen Free, Trial, Basic und Premium verlangten ein manipulationssicheres System, das Testphasen kontrolliert und Zahlungsabbrüche ohne Datenverlust verarbeitet.
Aufgabe
Ziel des MVP (Deadline: 16. Mai 2026) war eine nahtlos integrierte, extrem performante Full-Stack-Web-App. Funktional brauchte es ein interaktives Ernährungstagebuch mit Live-Nährwertskalierung und Barcode-Lookup per Kamera, ein visuelles Dashboard für Gewicht, Maße, Schlaf und Aktivität über frei wählbare Zeiträume und einen 14-tägigen, kartenlosen Premium-Testzugang, der pro Nutzer:in strikt einmalig ist. Die technischen Herausforderungen: In Express 5 ist req.query eine schreibgeschützte Getter-Eigenschaft, sodass Typ-Konvertierungen aus Validierungsbibliotheken standardmäßig verloren gehen und Datenbank-Aggregationen zum Absturz bringen; Stripe-Webhooks mussten gegen Race-Conditions und mehrfach gefeuerte Zustellungen gehärtet werden; und das mächtige Admin-Dashboard durfte das initiale Laden der regulären App nicht verlangsamen.
Umsetzung
Das Frontend hält das Access Token ausschließlich im flüchtigen Arbeitsspeicher; läuft es ab (15 Min), fängt ein Axios-Interceptor den 401-Fehler ab, stößt im Hintergrund über das httpOnly-Cookie (/auth/refresh) einen Silent Refresh an und wiederholt die ursprüngliche Anfrage unbemerkt. Ändert ein:e Nutzer:in das Passwort, wird das gesamte refreshTokens[]-Array in der MongoDB geleert und alle anderen Sessions werden plattformweit sofort ungültig. Die Lebensmittelsuche läuft über eine kaskadierende Fassade in nutritionService.ts – OpenFoodFacts → FatSecret (OAuth 1.0) → USDA FDC → eigene Custom Ingredients – bis Treffer erzielt werden. Der Stripe-Webhook ist strikt und Ack-First: Signaturprüfung über den Raw-Body, Idempotenz-Marker (eindeutiger eventId-Index; ein Duplicate-Key-Fehler bricht sofort mit 200 OK ab), innerhalb von ~100 ms received:true zurücksenden, um Retries zu vermeiden, und erst danach das Event asynchron verarbeiten – bei Fehlern wird der Marker gelöscht, damit der Stripe-Retry greift. Im Frontend wird das gesamte Admin-Dashboard per React.lazy nachgeladen, sodass reguläre Nutzer:innen es nie herunterladen, monolithische Seiten wurden in fokussierte Hooks und Panels aufgeteilt (die Food-Diary-Seite schrumpfte von 991 auf 278 Zeilen), und die Nährwertskalierung (scaleFood) ist zentralisiert, sodass Scanner, Favoriten und Suche dieselbe Logik nutzen.
Ergebnis
Der kaskadierende API-Ansatz und ein Snapshot-Pattern bei Rezepten (Nährwerte werden beim Hinzufügen zum Rezept eingefroren) halten das Ernährungstagebuch konsistent, selbst wenn das zugrundeliegende Lebensmittel später editiert oder gelöscht wird. Der kartenlose Testzugang senkt die Einstiegshürde massiv – keine Zahlungsdaten für 14 Tage Premium – während ein persistentes hasUsedTrial-Flag am User-Model die mehrfache Ausnutzung blockiert. Das Admin-Panel berechnet demografische und fortschrittsbasierte Kennzahlen (z. B. avgWeightLossPct, Kohorten-Ernährungsdurchschnitte) und ermöglicht dem Betreiber datengetriebene Anpassungen des 12-Wochen-Programms.
Fazit & Lessons Learned
Ein paar Express-/Mongo-Fallstricke prägten die Umsetzung. Da req.query in Express 5 ein Getter ist, schlagen delete und Object.assign still fehl und geänderte Zod-Werte kamen nie in den Controllern an – gelöst, indem eine validateQuery-Middleware das gesamte query-Objekt via Object.defineProperty hart neu definiert. Mongoose castet String-IDs bei normalen Abfragen automatisch zu ObjectIds, in Aggregations-$match jedoch nicht, sodass Pipelines leere Arrays lieferten, bis IDs explizit mit new mongoose.Types.ObjectId(userId) gecastet wurden. Fehlende optionale Werte (Schritte, Distanz) ließen ganze Wochensummen zu null mutieren, bis $ifNull konsequent eingesetzt wurde. Und Express matchte /recipes/:id vor /recipes/range und interpretierte „range“ als ID – behoben, indem statische Routen stets vor dynamischen registriert werden.