How do you evolve an API without breaking existing clients?
Every field you add is free; every field you remove, rename, or retype is a promise broken to someone you've probably never met — evolving an API well means treating the contract as public even when it isn't.
LEARN BY DOINGPut your intuition to work3 steps
How do you evolve an API without breaking existing clients?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Why interviewers ask this
Anyone can design an API's first version. What separates a senior engineer is what happens in version two, three, and every incremental change after that — because by then, real clients you don't fully control (a mobile app you can't force-update, a partner's integration, an internal service three teams removed from you) are already depending on the exact shape of what you shipped. This question checks whether you think about a response body as a promise to a stranger, or as an implementation detail you're free to rearrange.
#Postel's law as the operating principle
The classic framing — "be conservative in what you send, liberal in what you accept" — translates directly into API design discipline. On the server side, "conservative in what you send" means never removing, renaming, or retyping something a client might already be reading, because you cannot verify that nobody depends on it; on the client side, "liberal in what you accept" means a well-built client ignores fields it doesn't recognize and tolerates optional fields being absent, rather than failing on anything unexpected. Both halves matter — a disciplined server paired with a fragile client will still break on a perfectly safe additive change, which is why "tolerant reader" is as much a client-engineering discipline as versioning discipline is a server one.
#The real taxonomy: additive vs. breaking
| Safe (additive) | Breaking |
|---|---|
| Adding a new optional response field | Removing a field, even one that "looks unused" |
| Adding a new optional request parameter | Renaming a field |
| Adding a new endpoint | Changing a field's type (string → number) |
| Adding a new, non-default enum value to a request the client already sends | Adding a new enum value to a response the client switches on exhaustively |
| Relaxing a validation rule (accepting more) | Tightening a validation rule (rejecting what used to be valid) |
| Making a required request field optional (with a sane default) | Making an optional request field required |
| Adding a new response header | Changing a default (page size, sort order, timezone) without a flag |
| Deprecating with warning headers, old behavior unchanged | Changing status codes for an existing success/error case |
#Versioning: pick the escape hatch you'll actually need
Additive-only discipline delays the need for a version bump for a long time, but it doesn't eliminate it — eventually a genuinely breaking change (a security fix, a modeling mistake that has to be corrected) is unavoidable. The three common strategies trade off differently:
| Strategy | Example | Pro | Con |
|---|---|---|---|
| URI path | /v2/orders | Obvious, cacheable, easy to route at the gateway | Encourages "just fork everything" instead of minimal, targeted changes |
| Header | Api-Version: 2 | Keeps URLs stable, plays well with REST's resource-identity idea | Easy to forget, harder to test/cache/debug from a browser bar |
| Media type | Accept: application/vnd.shop.v2+json | Most "correct" per HTTP content negotiation | Least ergonomic for consumers, rarely used outside API-purist shops |
Whichever mechanism you pick, the goal is to need it as rarely as possible — most "do we need v3" conversations are actually solved by realizing the change is additive after all, or by isolating the one breaking field behind a new optional one and deprecating the old one on its own timeline instead of forking the whole resource.
#Deprecation is a process, not a header
Announcing a breaking change well is what actually protects clients, not the versioning mechanism itself. A responsible deprecation communicates through multiple, redundant channels — a changelog entry, direct outreach to known consumers if you can identify them, and machine-readable signals in the response itself:
The Deprecation header signals "this still works today, but shouldn't be used for new code," while the Sunset header (RFC 8594) gives a concrete date after which the old behavior may stop working entirely — both are checkable by automated tooling on the consumer's side, not just readable by a human skimming docs.
#Catching accidental breakage before it ships
None of this works if a breaking change can reach production without anyone noticing until a client's crash reports spike. The gap is closed by making the previous section's discipline enforceable rather than aspirational: contract tests (provider-driven via OpenAPI, or consumer-driven via something like Pact) run in CI against the real implementation, and a schema-diff tool compares each new spec against the last published one, failing the build automatically when a field disappears, a type narrows, or a previously-optional field becomes required. Treat that failure exactly like a failing unit test — an engineer has to consciously override it (bump the major version, update the changelog, start the deprecation clock), never silently pass through it.
🧠 Check your understanding
0/2 · 0/2 answered1. Your OrderStatus field can be pending, paid, shipped, or cancelled. You add a fifth value, refunded, to the response. Why can this break existing clients even though nothing was removed?
2. Which of these is the safest way to communicate that an endpoint will be removed in 90 days, without breaking anyone today?