By Huzefa Motiwala · Co-Founder & Chief Product Officer

TL;DR
A public API on a legacy SaaS breaks in six predictable places: it leaks your internal data model as a permanent contract, it has no per-client rate limiting, its auth was built for one trusted tenant not many untrusted ones, every schema change becomes a breaking change, retries create duplicate writes, and external traffic hits code paths that were never load-tested. None of these are the API itself failing. They are the system underneath discovering it was never designed to be spoken to directly.
We get called in for this after the fact more often than before it. A team ships an API because a big customer asked for one, wires it straight onto the existing service layer, and three months later a single integrator’s retry loop is duplicating orders in production. The API worked in the demo. It broke under the reality of strangers depending on it.
This piece is about what actually breaks, and the pattern that lets you add a public API to a system teams are afraid to touch, without rewriting the core.

Six things break, and they break in a specific order: data model, limits, auth, versioning, idempotency, then performance. The first four are design mistakes you bake in on day one. The last two only surface once real third-party traffic arrives, which is exactly when they are hardest to fix. The table below is the short version we walk clients through before writing any endpoint.
| What breaks | Why the legacy system can’t handle it | Mitigation |
|---|---|---|
| Internal data model leaks | Serialisers return DB rows straight to the client | Contract-first DTOs behind an anti-corruption layer |
| No rate limiting or quotas | Only ever served your own trusted frontend | Per-key limits and usage plans at the gateway |
| Auth built for one tenant | Session or single API secret, no scoping | OAuth2 or scoped keys, tenancy enforced at the edge |
| Every change is a breaking change | No versioning, clients read every field | Versioned contract, additive changes only |
| Duplicate writes on retry | POST handlers assume one call per action | Idempotency keys required on all writes |
| Collapses under external load | Synchronous paths tuned for internal volume | Gateway caching, read replicas, backpressure |
Because the fastest way to ship an API is to serialise whatever objects you already have, and those objects are your database rows. The moment an external developer sees internal_status_code or a foreign key you meant to rename, that field is a contract. You now cannot refactor your own schema without breaking their integration, and you will not know who depends on which field.
The fix is an anti-corruption layer: a translation boundary that maps your messy internal model into a clean, deliberate public shape. Microsoft’s pattern catalogue frames it as a facade that isolates the two subsystems so foreign concepts never leak across. The public API speaks its own vocabulary; the legacy database keeps its own. Change one without touching the other.
Write the OpenAPI spec first, review it as the product it is, then implement against it. A contract-first API is a decision about what you are willing to support for years. It is not a reflection of your current tables. This is the single highest-leverage habit we push on teams adding their first external surface, and it is the one most often skipped under deadline.
You add them at a new layer in front of the system, not inside it. Legacy auth was usually built for one trusted context: a logged-in session, or a single shared secret for a partner. External developers break both assumptions. They share keys across environments, leak them into client-side code, and hammer endpoints with no backoff. The gateway is where you handle that reality.

An API gateway sits between third-party clients and your system and enforces the things the legacy core cannot:
The rule we keep coming back to: enforce identity and coarse scope at the gateway, enforce fine-grained business rules in the application. Mixing the two layers is the most common pitfall, and it is where security holes hide.
A gateway in front, an anti-corruption layer to translate, a facade over the legacy core, and the whole thing added incrementally rather than in one cut. This is the strangler fig approach applied to an API surface: you route external requests through a new facade that initially just forwards to the existing system, then peel real logic into the new layer endpoint by endpoint.
Martin Fowler’s original point is that investment and returns happen gradually and visibly, so you are never one big-bang deploy away from an outage. The facade intercepts every external call. Behind it, the anti-corruption layer shields the public contract from legacy semantics. The legacy system barely knows the API exists. We have used this exact spine on systems where a full rewrite was off the table, and it pairs naturally with a feature-flag rollout so you can move one endpoint’s traffic at a time.
Two habits make the incremental version work in practice. Map what depends on what before you start, because the dependency graph almost always changes your estimate. And treat the cutover as a deployment problem, not just a coding one, which is where zero-downtime modernisation architecture earns its keep. Teams that skip the mapping step tend to repeat the strangler fig mistakes that undo the whole plan in the first three months.
Two disciplines: version the contract so change is opt-in, and make every write safe to retry. Once external developers depend on you, a broken deploy is their outage and your support queue. Both problems are cheap to prevent at design time and expensive to retrofit once integrations are live.
Put a version in the URL path or a header, and treat the current version as frozen. Add new endpoints instead of changing existing ones. Add new response fields instead of renaming old ones. API versioning guidance is consistent on this: additive minor changes are backward-compatible, while altering a response structure or removing an endpoint is a breaking change that disrupts everyone relying on the API. In our own client work every field a client already reads is a promise, and teams that break them casually lose integrators to more stable competitors.
External clients retry on every timeout, and without protection each retry is a second order, a duplicate charge, a phantom record. Stripe’s idempotency design is the reference: the client sends a unique key with each write, the server stores the first result against that key, and repeated calls return the saved response instead of acting twice. Stripe’s API reference removes keys after they are at least 24 hours old, long enough to cover any sane retry window. Require an idempotency key on every POST and you remove a whole class of production incidents before it appears.
If your product exposes real-time or high-frequency data, the same principles apply with more pressure on caching and backpressure, something we covered for a related domain in our real-time data API integration guide.
When you cannot yet name the fields you are willing to support for three years, or when the legacy core has no isolation between tenants and no path to add it. A public API is a long-term commitment dressed up as a feature. If the underlying system is still changing shape weekly, you will freeze it prematurely and spend the next year maintaining versions you regret. Sometimes the honest answer is to stabilise the core first and expose the API second.
The teams that get this right treat the API as a product with its own contract, its own release cadence, and its own boundary. The ones that struggle treat it as a thin serialiser over the database and discover, one duplicate order at a time, that they shipped their internal model to the world.
Yes. You do not need microservices to expose a public API. Put a gateway in front for auth and rate limiting, add an anti-corruption layer to translate your internal model into a clean public contract, and route external calls through a facade over the existing monolith. The strangler fig pattern lets you do this incrementally, so the monolith keeps running untouched while the API surface grows in front of it.
It is a translation boundary between your public API contract and your internal system. Instead of serialising database rows directly to clients, the anti-corruption layer maps your messy internal model into a deliberate public shape and back again. That way you can refactor your schema without breaking anyone’s integration, because external developers only ever see the stable public vocabulary, never your tables.
An internal API only serves your own frontend, which behaves predictably. A public API serves strangers whose implementation quality you cannot control, including retry loops with no backoff. Without per-client rate limiting and quotas at the gateway, one badly written integration can saturate your origin and take down production for every customer. Limit on the API key rather than the IP, since many clients share an IP.
The client generates a unique key for each write operation and sends it with the request. The server stores the result of the first call against that key. If a network failure triggers a retry with the same key, the server returns the saved response instead of performing the action again. This is how Stripe lets clients safely retry payments without double-charging. Require a key on every POST endpoint.