This blog post is also available in English

TL;DR

  • SPA-first-APIs geben interne Details der Domäne oft an integrierende Systeme weiter.
  • Wer Frontend-Endpunkte veröffentlicht, geht ein implizites Stabilitätsversprechen ein, das er sicherlich brechen wird.
  • Selbst rein additive Änderungen tun weh, wenn Konsument:innen von Änderungen am Modell wissen müssen.
  • Trenne UI-APIs und halte sie privat, getrennt von einer öffentlichen Integrations-API, um die Kopplung gering zu halten.

Ein einfaches Domänenmodell in Aktion

Werfen wir zunächst einen Blick auf ein einfaches Domänenmodell in Aktion. Stell dir vor, du erstellst ein Modell, mit dem ein Ladenbesitzer seine Öffnungszeiten eingeben kann. Unsere Domänenexpert:innen denken über ihre Öffnungszeiten vielleicht in drei Konzepten: Es gibt reguläre Öffnungszeiten, Feiertage und Sonderöffnungszeiten. Reguläre Öffnungszeiten (Regular Periods) werden zum Beispiel so definiert: „Im Sommer öffnen wir von Montag bis Freitag von 9:00 bis 12:00 und von 13:00 bis 20:00 Uhr sowie Samstags von 9:00 bis 12:00 Uhr.“ Dann gibt es Feiertage, an denen alle Läden schließen müssen, etwa den Tag der Arbeit (1. Mai in Deutschland). Schließlich kann es einmalige Sonderöffnungszeiten (Overrides) geben, bei denen die regulären Öffnungszeiten für einen Tag außer Kraft gesetzt werden (z. B. früher schließen wegen Personalmangels).

Unsere Anwendung bildet diese Konzepte, in denen unsere Domänenexpert:innen denken und ermöglicht es so, Öffnungszeiten durch das Anlegen, Ändern und Löschen von «Regular Periods», «Overrides» und «Holidays» zu pflegen, wie in der UI unten zu sehen:

Öffnungszeiten-Kalender (4.–10. Mai 2026): Di/Mi/Fr 9:00–17:00, Mo 9:00–15:30, Do 9:30–15:30; Sa/So geschlossen.
Eine SPA zeigt den Nutzern die Öffnungszeiten an

Unsere UI (in diesem Fall eine React-Anwendung) nutzt den Endpunkt /api/opening-hours, der nach Start- und Enddatum filtert. Das Antwortformat dieses Endpunkts sieht so aus:

{
  "regular_periods": [
    {
      "name": "Summer Schedule 2026",
      "starts_on": "2026-03-01",
      "ends_on": "2026-08-31",
      "weekdays": {
        "monday": [
          {
            "opens_at": "09:00",
            "closes_at": "12:00"
          },
          {
            "opens_at": "13:00",
            "closes_at": "20:00"
          }
        ],
        "tuesday": [
          {
            "opens_at": "09:00",
            "closes_at": "12:00"
          },
          {
            "opens_at": "13:00",
            "closes_at": "20:00"
          }
        ],
        "wednesday": [
          {
            "opens_at": "09:00",
            "closes_at": "12:00"
          },
          {
            "opens_at": "13:00",
            "closes_at": "20:00"
          }
        ],
        "thursday": [
          {
            "opens_at": "09:00",
            "closes_at": "12:00"
          },
          {
            "opens_at": "13:00",
            "closes_at": "20:00"
          }
        ],
        "friday": [
          {
            "opens_at": "09:00",
            "closes_at": "12:00"
          },
          {
            "opens_at": "13:00",
            "closes_at": "20:00"
          }
        ],
        "saturday": [
          {
            "opens_at": "09:00",
            "closes_at": "12:00"
          }
        ],
        "sunday": []
      },
      "id": "8b8633b8-c027-4af6-8e6d-a22edf4faecb"
    }
  ],
  "holidays": [
    {
      "date": "2026-05-01",
      "name": "Labour Day",
      "id": "0bc5c05a-b8c2-5583-aed4-4f58d00bd050"
    }
  ],
  "day_overrides": [   
    {
      "date": "2026-04-30",
      "timespans": [
        {
          "opens_at": "09:00",
          "closes_at": "11:00"
        }
      ],
      "note": "Prepare for Labour Day",
      "id": "fe95781e-f83e-4f27-a404-da43fc537ff6"
    }
  ]
}

Unsere UI weiß, wie diese Einträge sich gegenseitig aufheben oder überschreiben, und rendert den Kalender entsprechend, indem sie z. B. bei Overrides die Regular Hours ausblendet. Die UI muss dafür wissen, wie das Domänenmodell aufgebaut ist. Zudem brauchst du Endpunkte, um jedes dieser unterschiedlichen Domänenkonzepte gegebenenfalls zu aktualisieren und zu löschen. Am Ende könnte die API, die du für dein Frontend entworfen hast, etwa so aussehen:

Swagger-artige API-Liste „opening-hours“ mit GET/POST/PUT/DELETE-Endpunkten für regular-periods, holidays und day-overrides.
Die Swagger-Dokumentation der vom Frontend genutzten API

So weit, so gut.

Wenn externe Systeme integriert werden müssen

Stell dir nun vor, ein anderes Team benötigt diese Öffnungszeiten. Kein Problem! Wir haben ja bereits all diese API-Endpunkte, und fastapi hat uns freundlicherweise bereits diese OpenAPI-Spezifikation erstellt. Wir reichen sie einfach weiter, kein Aufwand für uns! Nicht so schnell. Die API deines Frontends weiterzugeben ist eine Entscheidung, die du vielleicht noch bereust.

Indem du diese Endpunkte an ein anderes Team weitergibst, sagst du: „Auf diese Endpunkte ist Verlass, sie ändern sich nicht.“ Doch früher oder später ändern sie sich eben doch. Nahezu jedes Feature deiner Anwendung geht schließlich mit einer Änderung der UI und der dazugehörigen API einher. Und wenn es so weit ist, musst du die Integration mit allen Systemen testen, die einen dieser Endpunkte möglicherweise genutzt haben, denn es gibt keine einfache Möglichkeit festzustellen, ob einer der Endpunkte von einer anderen Anwendung verwendet wird und diese durch deine Änderung kaputtgegangen ist.

Man könnte versuchen, der API nur noch optionale Felder und Endpunkte hinzuzufügen, um das Versprechen bestehender Endpunkte nicht zu brechen. Doch auch diese «additiven Änderungen» können sich auf subtilere Weise als «breaking change» erweisen. Erinnerst du dich an unsere drei verschiedenen Arten von Öffnungszeiten in der API (Regular Intervals, Overrides und Holidays)? Was passiert, wenn wir eine vierte hinzufügen? Da wir unsere Clients mit den Konzepten unseres Modells belastet haben, muss jede:r Konsument:in unserer API neue Konzepte und auch neue Vorrangregeln lernen, auch wenn er eigentlich nur an den effektiven Öffnungszeiten interessiert ist.

Das Problem ist, dass unsere API zu viel vom Domain Model zeigt. Die API, die wir für unsere SPA erstellt haben, legt Implementierungsdetails offen, die unsere API-Konsument:innen gar nicht interessieren sollten. Sie hatte nie die Systemintegration als Entwurfsziel, sondern wurde mit Blick auf andere Konsument:innen und deren Bedürfnissen entworfen.

Die API unserer UI muss von den verschiedenen Arten von Öffnungszeiten wissen. Andere Teams interessieren sich höchstwahrscheinlich nur für die „effektiven“ Öffnungszeiten, sie wollen einfach wissen, wann ein Laden öffnet oder schließt. Das ist eine andere Abstraktionsebene. Und die API unserer UI bietet diese nicht.

Deshalb sollten wir die APIs besser trennen. Wir erstellen eine (private) API für unsere UI und eine (öffentliche) API für die Systemintegration. Jetzt wirst du vielleicht einwenden: Das ist doch Duplikation! Und Duplikation ist immer böse! In diesem Fall würde ich argumentieren, dass uns die Duplikation etwas viel Wichtigeres verkauft: lose Kopplung. Wir können unsere API für die SPA jetzt ändern (was wir höchstwahrscheinlich täglich tun), ohne alle anderen Systeme erneut testen zu müssen. So sähe eine Trennung der Schnittstellen (interface segregation) aus:

Diagramm: Externes System nutzt die Public API zum Backend; das Frontend «SPA» nutzt die Private API innerhalb der „Grenze deines Domänenmodells“.
Dein eigenes Frontend darf dein Domänenmodell kennen, andere API-Konsument:innen und externe Systeme sollten das nicht.

In unserer OpenAPI-Doku haben wir jetzt ebenfalls zwei Sätze von Endpunkten, die öffentlichen:

Swagger UI der „Opening Hours Public API“ mit den Endpunkten GET /api/opening-hours und GET /health.
Die OpenAPI-Dokumentation nach der Schnittstellentrennung, nur der öffentliche Teil.

und die privaten:

Swagger UI der „Opening Hours Private API“ mit Endpunkten für opening-hours, regular-periods, holidays und day-overrides.
Die OpenAPI-Dokumentation nach der Schnittstellentrennung, mit dem privaten Teil.

Durch zwei verschiedene Sätze von APIs können wir erkennen:

  • Welche Endpunkte wir gefahrlos ändern können, ohne einen neuen Systemintegrationstest durchzuführen
  • Welche Implementierungsdetails vor anderen Teams verborgen sind und was wir ändern können, ohne die Kompatibilität zu brechen

Außerdem können wir unterschiedliche Bedürfnisse effektiver bedienen: Vielleicht möchte einer unserer Konsument:innen Änderungen an unseren Öffnungszeiten «pollen», weil er eine lokale Kopie vorhält, um von der Verfügbarkeit unseres Systems unabhängig zu sein. Etwas, wofür unser aktuelles API-Design vielleicht nicht ohne Weiteres geeignet ist.

Fazit

Gute APIs sollten in erster Linie an den Bedürfnissen ihrer Konsument:innen ausgerichtet sein. Diese Bedürfnisse unterscheiden sich zwischen SPAs und anderen Backends, die Daten zu Integrationszwecken benötigen. Und auch wenn es sich seltsam anfühlt, mehrere Endpunkte zu erstellen, die dieselben Daten anbieten: In einer zunehmend verteilten Welt ist es viel wichtiger, den Bedürfnissen der Systeme gerecht zu werden und stabile, sich selten ändernde Abstraktionen zu bauen, als „Code nicht zu duplizieren“. Denn die verteilte Duplikation von Geschäftslogik (das Berechnen der effektiven Öffnungszeiten), die sich einschleichen kann, wenn wir die wahren Bedürfnisse der Kund:innen ignorieren, ist in der Konsequenz viel schlimmer als eine gefühlte Duplikation bei Endpunkten, die „dasselbe zurückgeben“.

Ich habe eine funktionierende (wenn auch durch KI-Unterstützung erstellte) Beispiel-Codebasis, auf der dieser Artikel basiert, auf Codeberg hochgeladen.