When a legacy system is up for modernization, the first proposal is often a new front end. That is understandable: the interface is what everybody sees, and it looks the oldest.
It is still the wrong order. A new front end on a system with no API means tying the new interface to the same page requests as the old one, only with more JavaScript in between. The rebuild costs months and changes nothing about how changeable the system is.
The order that holds is the reverse: an interface first, then everything that can hang off it. This article describes what that layer looks like, where it gets cut, and what it explicitly must not do.
Why the interface comes before the front end
A monolith serving HTML has exactly one consumer: the browser. Everything else that needs data improvises: a CSV export, a script reading the database directly, a page somebody scrapes.
An interface changes that fundamentally, not because of the technology but because of the number of options afterwards. With one, three things become possible that were not: a new interface, an extracted service, an integration by a partner. Without one, each of those three is its own rebuild of the monolith.
The practical difference shows at the first extracted service. If an interface exists, extraction is a redirect: the new service serves the same calls. Without one, the service has to rebuild what the monolith does inside its page requests, and that is far more work.
What this layer is, and what it is not
The layer is code inside the monolith: a set of endpoints calling the same logic as the existing pages and returning the results as JSON. It is not an additional system.
Three boundaries save money.
No gateway product at the start. A managed gateway solves problems that do not exist with one consumer and ten endpoints: per-customer throttling, key management, usage billing. It arrives when there is a reason, not before.
No complete mapping of the system. The reflex to create endpoints for every table produces a hundred endpoints of which three get used. You build what the first consumer needs.
No rebuild of the existing pages. The old pages stay as they are. The interface sits next to them, not underneath, and that is exactly why it is finished in weeks rather than months.
The same cut later decides which part gets pulled out first: Which service to pull out of the monolith first.
The cut: by use case, not by table
The most important decision is what the endpoints represent. Two routes are open, and the obvious one is the worse one.
The obvious one mirrors the database: /api/customers, /api/orders, /api/lines. That is quick to write and pushes the work to the consumer: for an order overview it needs three calls and has to know how they fit together. Which means domain logic sits in the front end, and with the second consumer it sits there twice.
The better one mirrors what somebody wants to do:
GET /api/v1/orders?status=open&since=2026-11-01
GET /api/v1/orders/{no} # with lines and customer
POST /api/v1/orders/{no}/cancellation
GET /api/v1/customers/{no}/open-itemsThe third entry is where the two routes part. A cancellation is not changing a field, it is an operation with rules: it is only allowed in certain states, it creates a credit note, it notifies the warehouse. As its own endpoint those rules stay in the monolith, where they already are. As a PATCH on a status field the consumer would have to know them.
The rule for it: every endpoint that changes state represents a business operation, not a record. For reading, the data view is fine; the damage there is small.
Versioning, but small
The /v1/ in the path costs nothing and saves an unpleasant discussion in two years. More important than the mechanism is the promise behind it, and it fits in three sentences.
- New fields may appear at any time. Consumers ignore what they do not know. That belongs in the description of the interface, so nobody relies on the response having exactly these fields.
- Fields do not disappear within a version. A field that is no longer maintained stays and gets marked as deprecated.
- A new version exists only for breaking changes, and the old one keeps running until consumers have moved. Anybody unwilling to do that does not create a new version.
For the description itself, an OpenAPI file in the repository is enough. It does not have to be pretty; it has to be current, and for that it belongs next to the code rather than in a wiki.
Authentication: where it falls over
This is where most projects of this kind buckle, and the reason is always the same: the monolith has session login with cookies, and that does not fit an interface.
Three routes are open, and the choice depends solely on the first consumer.
Reuse the session. If the first consumer is a new interface on the same domain, the existing login works unchanged. That is the fastest route and it carries surprisingly far. It ends as soon as a consumer comes from outside.
Static keys per consumer. For another in-house system or a partner. Unspectacular, sufficient and built in an hour, as long as the keys are per consumer and can be revoked individually.
Tokens with expiry. For anything involving user identity that does not run in a browser on the same domain. The extra effort is real and only pays off then.
The recommendation is the simplest variant sufficient for the first consumer, and an explicit refusal to build the third one straight away. Changing later is smaller than it seems: it affects one place in the layer.
What does belong there from the start is the separation of authentication from authorization. Who has a right is decided by the monolith with its existing logic. The layer only checks who is there and passes the question of permission on.
What the layer must not do
The layer is translation, not domain logic. That sounds obvious and gets violated regularly, because it is convenient.
The typical course: the consumer needs a total that does not exist in the monolith. It is quickly calculated in the endpoint, so it gets calculated there. Three months later there are two truths about the same number, because the page in the monolith calculates differently.
// Wrong: the layer calculates. The page in the monolith calculates
// differently, and in six months nobody knows which number is right.
$open = 0;
foreach ($orders as $o) {
if ($o['status'] !== 'paid') {
$open += $o['amount'] - $o['credit'];
}
}
// Right: the domain logic stays where it already lives. If it is missing
// there, it gets added there and used by both sides.
$open = $this->openItemsCalculator->forCustomer($customerId);The test for it is simple: if an endpoint returns a number that appears on no page of the monolith, domain logic is being moved into the layer. That is not always wrong, and it is always worth a deliberate decision.
Whether a new front end is needed at all is a question of its own: Modernizing a server-rendered front end without an SPA.
What becomes possible afterwards
The effort for a first interface is two to four weeks for ten to fifteen endpoints, provided the logic in the monolith is callable. What becomes possible afterwards justifies it.
A new interface becomes a project of its own. It hangs off the API, not off the monolith, and it can appear page by page while the old pages keep running.
An extracted service becomes a redirect. When a part later moves into its own service, nothing changes for the consumer: the same call lands somewhere else. Without an interface the same thing would be a rebuild at every consumer.
Integrations cost days rather than weeks. The partner wanting to collect orders, the other in-house system, the analytics: all of that becomes calls rather than bespoke solutions, and no further direct query into the database appears.
That last point is the underrated one. Every direct database access from outside is a contract nobody signed and one that makes every later schema change more expensive. An interface is the place where such access gets collected in, one consumer at a time.
How I build interfaces into existing systems is on its own page. And if the question after that is which part gets extracted from the monolith first: what that looks like technically next to a PHP system is covered in its own article.
This article belongs to a series about systems that already exist. The retrospective orders every article in it by situation.

