Dieser Blogpost ist auch auf Deutsch verfügbar
TL;DR
- SPA-first APIs often leak domain internals to integration consumers.
- Publishing frontend endpoints creates an implicit stability contract you will break.
- Additive changes still hurt when consumers need to know about changes in the model.
- Split UI APIs and keep them private from a public integration API to keep coupling low.
A simple domain model in action
First let’s explore our domain model in action. Imagine you are creating a model for a shopkeeper to enter their opening times. Our domain experts may think about their opening times in terms of three concepts: they have regular opening times, public holidays and overrides. Regular opening times are defined as, fore example “During summer we open from 9:00 to 12:00 and 13:00 to 20:00 on Monday to Friday and 9:00 to 12:00 on Saturdays”. Then there are public holidays where all shops need to close, such as Labour Day (May 1st in Germany). Lastly there may be one-off overrides where the regular opening hours are overridden for a day (e.g. closing early due to staff shortages).
Our application faithfully captures these domain concepts and enables domain experts to create, modify and delete their schedules as regular hours, overridden days and holidays, as seen in the UI below:  uses an endpoint called /api/opening-hours that filters by start and end date. The response format for this endpoint would look something like this:
{
"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"
}
]
}Our UI of course knows how different opening hours take precedence and renders the calendar accordingly, e.g. by hiding the overridden regular opening hours. It naturally needs to know about their existence and type, because a user might notice: “That’s not right! I should delete that override on Monday the 27th.” and be able to do so from your UI. You will also require endpoints to update and delete each of these different domain concepts. In the end the API you designed for your frontend might look something like this:
So far, so good.
When external systems need integration
Now imagine another team is in need of these opening hours. No problem! We already have all these API endpoints, and fastapi helpfully created this OpenAPI spec for us. We’ll just hand it over, no work required on our part! Not so fast. Handing over the frontend’s API is a decision you might come to regret.
By handing these endpoints to another team you are saying “you can count on these not changing” - but change they eventually will. And when they do, you will have to re-test with all the systems that may have used any of these endpoints because there’s no easy way to tell if any of the endpoints is being used by another application.
Now you are pretty much stuck with only adding optional fields and endpoints to the API, so as not to break the promise. But these additive changes may also turn out to be unsafe in a more subtle way. Remember our three different kinds of opening hours in our API (regular opening hours, overrides and holidays)? What happens when we add a fourth? Since we exposed our clients to these concepts, every consumer of our API needs to learn about new concepts and also the new precedence rules, if they want to know about the effective opening hours.
The real problem is our undue exposure of details. The API we created for our SPA is exposing implementation details that our API consumer should not even care about. That’s because it was created with a different consumer in mind.
Our UI’s API needs to know about different types of opening hours. Other teams probably only care about the “effective” opening hours - they just want to know when a store opens or closes. That’s another level of abstraction. And our UI’s API does not offer that.
That’s why we should segregate the APIs. Create one (private) API for our UI and one (public) API for systems integration. Now you might interject: that’s duplication! Duplication is always evil! In that case I would argue duplication actually gives us something much more important: loose coupling. We can now change our API for the SPA (something we most likely do daily) without needing to re-test all the other systems.
Here’s what interface segregation would look like:
In our OpenAPI docs we also have two sets of endpoints now, the public ones:
and the private ones:
By having two different sets of APIs we can tell:
- Which endpoints we can safely change without doing a new system integration test
- Which implementation details are hidden from other teams and what we can change without breaking compatibility
Also we’re able to cater to different needs more effectively: maybe one of our consumers actually wants to poll changes in our opening hours because they keep a local copy to be independent of our system’s availability. Something our current API design may not be suitable for.