Neu veröffentlicht: E-Commerce mit Power Pages, Stripe & Analytics

· Webentwicklung  · 5 minuten Lesezeit

Wenn Backend und Frontend unterschiedliche Typen kennen

Ein TypeError im Browser nach einem Backend-Refactoring. Die Ursache ist kein Bug im Code, sondern ein veralteter Typ im Frontend, der die neue API-Response-Shape nicht kannte.

Ein TypeError im Browser nach einem Backend-Refactoring. Die Ursache ist kein Bug im Code, sondern ein veralteter Typ im Frontend, der die neue API-Response-Shape nicht kannte.

Inhalt

Der Fehler nach dem Deployment

Nach dem Phase-3-Refactoring des Backends war das Frontend nach dem Login schlicht leer. In der Browser-Konsole stand:

TypeError: Cannot read properties of undefined (reading 'pointCount')

Das ist die Art von Fehler, die keinen Stack-Trace produziert, der auf die eigentliche Ursache zeigt. pointCount ist undefined. Wo kommt pointCount her? Ich musste in den Code schauen.

Was geändert wurde und was nicht

Im Backend hatte Phase 3 den Health-Endpunkt überarbeitet. Die alte Response-Shape war:

// old shape
{
  status: "ok",
  provider: "ollama",
  services: { ollama: "ok", qdrant: "ok" },
  collection: { name: "instagram_memory", pointCount: 42 }
}

Das war die Collection-zentrierte Sicht: Ein System, eine Collection, ein Point-Count. Nach Phase 3 gibt es keine einzige globale Collection mehr. Jeder Nutzer hat seine eigene Collection. Die Health-Antwort zeigt jetzt die Qdrant-Konnektivität auf System-Ebene:

// new shape
{
  status: "ok",
  provider: "ollama",
  services: { ollama: "ok", qdrant: "ok" },
  qdrant: { collectionCount: 3 }
}

Das Frontend hatte davon nichts gewusst. Der TypeScript-Typ in api.ts war noch der alte:

// stale type in frontend/src/lib/api.ts
export type HealthResponse = {
  status: 'ok' | 'degraded';
  provider: string;
  services: Record<string, string>;
  collection: { name: string; pointCount: number }; // no longer exists in backend
};

Und in App.tsx wurde direkt auf dieses Feld zugegriffen:

<span>({health.collection.pointCount})</span>

health.collection ist undefined, weil das Backend kein collection-Feld mehr liefert. Und undefined.pointCount wirft einen TypeError.

Warum TypeScript das nicht verhindert hat

Die naheliegende Frage: TypeScript sollte doch genau das erkennen. Die Antwort: TypeScript prüft nur den Code, nicht den Laufzeitwert der API-Response.

Das Frontend definiert HealthResponse als TypeScript-Typ. Dieser Typ beschreibt, was das Frontend erwartet. Wenn das Backend eine andere Shape liefert, gibt es zur Kompilierzeit keinen Fehler. TypeScript vertraut dem Typ. Das Laufzeitobjekt entspricht dem Typ nicht mehr, aber das merkt niemand, bis der Nutzer die Seite öffnet.

Das ist ein klassisches Auseinanderlaufen von Backend-API-Vertrag und Frontend-Typ-Definition. Beide werden manuell gepflegt, und wenn das Backend sich ändert, muss das Frontend nachgezogen werden.

Die Korrektur

Zwei Stellen mussten angepasst werden.

Erstens der Typ in api.ts:

export type HealthResponse = {
  status: 'ok' | 'degraded';
  provider: string;
  services: Record<string, string>;
  qdrant: { collectionCount: number }; // updated to match backend
};

Zweitens die Render-Stelle in App.tsx:

<span>({health.qdrant.collectionCount})</span>

Der Build läuft durch. Die Seite zeigt nach dem Login die Anzahl der Qdrant-Collections an, was für ein Multi-Tenancy-System die sinnvollere Information ist als ein einzelner Point-Count.

Was ich daraus gelernt habe

Wenn sich eine Backend-Response-Shape ändert, reicht es nicht, nur den Backend-Code und die Backend-Tests zu aktualisieren. Der API-Vertrag muss durch alle Schichten synchron gehalten werden. In diesem Projekt bedeutet das: backend/src/routes/health.ts, backend/README.md, frontend/src/lib/api.ts, frontend/README.md und die betroffenen Komponenten.

Ein API-Schema-Tool wie zod mit z.infer<> oder ein OpenAPI-Generator würde dieses Problem strukturell lösen. Der Typ im Frontend würde aus dem Schema generiert, das das Backend definiert. Dann ist ein Typ-Mismatch kein Runtime-Error mehr, sondern ein Build-Fehler.

Für Phase 4 ist das eine sinnvolle Investition. Für jetzt: der Fix ist gemacht, die Seite lädt korrekt, und alle README-Dateien reflektieren die aktuelle Shape.

Sequenzdiagramm: Frontend lädt HealthResponse und liest collectionCount statt pointCount

Das Diagramm zeigt den Datenpfad von der Backend-Response bis zur Render-Zeile in React. Der veraltete Typ wirkte als stille Fehlerquelle, weil TypeScript die Diskrepanz erst zur Laufzeit sichtbar macht, nicht beim Kompilieren.


Alle Artikel der Serie

  1. Vision und Systemübersicht: Chrome Extension, RAG-Architektur, Projekthintergrund: Artikel lesen

  2. RAG-System Aufbau: Qdrant, Embeddings, Cosine-Ähnlichkeit in TypeScript: Artikel lesen

  3. AI Provider Abstraktion: Ollama vs. OpenAI, Interface-Design, kein Vendor-Lock-in: Artikel lesen

  4. Chrome Extension MV3: Drei isolierte Laufzeitkontexte, Message Passing, Strategy Pattern: Artikel lesen

  5. Docker Compose Strategie: Override-Pattern, von lokal zu Azure: Artikel lesen

  6. Ollama lokal vs. Docker: Die Entscheidung und ihre Konsequenzen: Artikel lesen

  7. Ollama Auto-Pull Entrypoint: Automatisiertes Modell-Setup beim Container-Start: Artikel lesen

  8. tsconfig und Vite: Node16 vs. bundler, warum Vite eigene Regeln hat: Artikel lesen

  9. Instagram Caption mit MutationObserver vollständig laden: Artikel lesen

  10. Chrome Extension Foundation mit Health-Dot und Retry-Queue: Artikel lesen

  11. Phase 2 Features: Shadow DOM Overlay, Tailwind v4, Duplicate Detection: Artikel lesen

  12. Race Condition bei der Plattformerkennung: Wie ein UI-Event die Instagram-Erkennung bricht: Artikel lesen

  13. PostId-Extraktion in zwei Instagram-Layouts: querySelector vs. Ancestor-Traversal: Artikel lesen

  14. Instagram Karussell vollständig erfassen mit MutationObserver: Lazy-Loading, Observer-before-click, Timeout-Fallback: Artikel lesen

  15. Notiz und Tags beim Screenshot-Speichern: Artikel lesen

  16. Instagram Tastatur-Shortcuts blockieren Chrome Extension Eingaben: Artikel lesen

  17. Lowercase-Normalisierung und Duplikat-Erkennung im Tag-Input: Artikel lesen

  18. Zitadel Login V2 in Docker Compose: drei versteckte Fehler: Artikel lesen

  19. PKCE OAuth in einer Chrome MV3 Extension: Artikel lesen

  20. React Frontend mit react-oidc-context und Zitadel: Artikel lesen

  21. Vite Build-Time-Umgebungsvariablen in Docker: Artikel lesen

  22. Event-Driven Ingestion mit BullMQ und Redis: Artikel lesen

  23. MinIO statt Azurite: S3-kompatible Objektspeicherung lokal und auf Hetzner: Artikel lesen

  24. access_token, id_token und der Userinfo-Endpoint: was wohin gehört: Artikel lesen

  25. Qdrant Multi-Tenancy: Pro Nutzer eine eigene Collection: Artikel lesen

  26. Wenn Backend und Frontend unterschiedliche Typen kennen (dieser Artikel)

  27. Zitadel Bootstrap entfernt: Host-Header-Bug und manuelles Setup: Artikel lesen

  28. Backend Code Review: sechs Probleme vor dem Launch behoben: Artikel lesen

  29. Traefik statt NGINX: Reverse Proxy für einen wachsenden Docker-Compose-Stack: Artikel lesen

  30. Zweischichtiges Rate Limiting: Traefik und express-rate-limit mit Redis: Artikel lesen

  31. DSGVO Art. 17 korrekt implementieren: Promise.allSettled und Export-Batching: Artikel lesen

  32. Embedding-Modell-Lock-in: Warum mxbai-embed-large eine Produktionsentscheidung für immer ist: Artikel lesen

  33. Docker Volumes in Produktion: Named Volumes, Bind Mounts und der Hetzner-Volume-Trick: Artikel lesen

  34. Zwei Sicherheitslücken vor dem Launch: Redis ohne Auth und ein offener Qdrant-Admin-Port: Artikel lesen

  35. Traefik als einziger Einstiegspunkt im Docker Compose Stack: Artikel lesen

  36. Zitadel hinter Traefik richtig verdrahten mit Issuer, JWKS und Login V2: Artikel lesen

  37. Frontend gesund machen wenn der nginx Healthcheck an localhost scheitert: Artikel lesen


Du baust ein TypeScript-Fullstack-System und willst API-Verträge zwischen Backend und Frontend automatisch synchron halten? Lass uns das gemeinsam einschätzen.

Zurück zum Blog

Ähnliche Beiträge

Alle Beiträge ansehen