API Versioning
How to change an API's shape over time without breaking the other applications and clients already relying on it.
What is it?
Once an API is live, you rarely control every single thing that calls it. A mobile app might be installed on thousands of phones, some of which won't update for months. A partner company might have built their own integration against your endpoints. If you change how an endpoint behaves — renaming a field, changing what a status code means, removing something you thought nobody used — every one of those existing callers can break the moment you deploy, with no warning.
API versioning is a strategy for making changes to an API while still letting existing clients keep working exactly as before, typically by letting multiple versions of an endpoint exist side by side, at least for a transition period. Two common approaches are URL versioning (/v1/users vs. /v2/users) and header versioning (the same URL, but the client specifies a version in a request header).
Explain like I'm 10
Think of a road under construction. You don't close the only bridge into town overnight and strand everyone driving toward it — you build a new bridge alongside the old one, let both operate for a while, and only remove the old one once you're confident nobody still needs it.
Examples
URL versioning
// v1: returns a flat "name" field
app.get("/v1/users/:id", (req, res) => {
res.json({ id: req.params.id, name: "Ada Lovelace" });
});
// v2: splits name into first/last, without touching v1's contract at all
app.get("/v2/users/:id", (req, res) => {
res.json({ id: req.params.id, firstName: "Ada", lastName: "Lovelace" });
});Old clients keep calling /v1/users and get exactly the response shape they always have, while new clients can opt into /v2/users for the improved shape.
Header-based versioning
app.get("/users/:id", (req, res) => {
const version = req.headers["api-version"] || "1";
const user = { id: req.params.id, name: "Ada Lovelace" };
if (version === "2") {
const [firstName, lastName] = user.name.split(" ");
return res.json({ id: user.id, firstName, lastName });
}
res.json(user);
});The URL never changes, but the client's requested version — sent as a header — determines the exact shape of the response.
How it works
Rather than changing an existing endpoint's behavior in place, a new version is introduced alongside it — either as a distinct URL prefix (/v2/...) or by reading a version identifier from a header (or sometimes a query parameter) and branching internally. Existing clients that don't specify a version, or that explicitly ask for the old one, keep getting the original behavior indefinitely (or until a communicated deprecation date), while new clients can adopt the new version whenever they're ready.
Why does it exist?
An API is a contract: whoever calls it is trusting that its shape and behavior won't shift unexpectedly under them. But software still needs to evolve — fields get renamed, response shapes improve, entire concepts get restructured. Versioning exists to let evolution and backward compatibility coexist, so an API owner can improve their design without holding every past decision hostage forever, and without breaking clients they don't control or can't instantly force to update.
When to use it
Introduce a new version when you need to make a breaking change — removing a field, changing a field's type or meaning, changing what a status code represents — to an API that already has external or independently-deployed consumers.
When not to use it
Purely additive changes — adding a new optional field, adding a brand new endpoint — generally don't require a new version at all, since well-behaved existing clients simply ignore fields they don't recognize. Versioning everything, even non-breaking changes, adds unnecessary maintenance overhead.
Common mistakes
Treating every single change as breaking and creating a new version far more often than necessary.
Introducing a new version but never actually retiring old ones, leaving an ever-growing pile of versions to maintain forever.
Changing an existing version's behavior 'just this once' instead of creating a proper new version, breaking clients who trusted that version to stay stable.
Practice exercises
- Easy:
Explain the difference between a breaking change and a non-breaking change to an API, with one example of each.
- Medium:
Design a /v1/ and /v2/ pair of routes for a
/products/:idendpoint where v2 adds a new required field that v1 didn't have. - Hard:
Propose a deprecation plan for retiring an old API version: how would you warn existing clients, and how would you decide when it's finally safe to remove it?
Interview questions
Why is API versioning necessary?
Because an API is a contract other systems depend on, and breaking changes to that contract can silently break clients you don't control — versioning lets you evolve the API while keeping existing consumers working.
What's the difference between URL versioning and header versioning?
URL versioning encodes the version directly in the path (like /v2/users), making it visible and cacheable; header versioning keeps one URL and lets the client specify a version via a request header, keeping URLs stable over time.
Does every change to an API require a new version?
No — only breaking changes (removing or restructuring existing fields, changing behavior clients rely on) typically require a new version; purely additive changes usually don't.
What generally counts as a breaking change to an API?
Removing or renaming a field, changing a field's type or meaning, changing what a status code represents, adding a new required request parameter, or restructuring the URL itself — any change an existing, well-behaved client couldn't simply ignore.
Give an example of a change that looks risky but usually isn't breaking.
Adding a brand-new optional field to a response. A well-behaved client only reads the fields it already knows about and ignores anything unfamiliar, so an additive field doesn't require a new version.
Why is URL versioning considered more visible than header versioning?
The version appears directly in the path itself, so it shows up in browser address bars, server logs, proxy configuration, and cache keys — there's nothing hidden that only shows up by inspecting request headers.
What's a downside of URL versioning?
Maintaining two (or more) full sets of routes for the same resource — /v1/users and /v2/users — means duplicated route logic and a growing surface area to keep working as more versions accumulate.
What's a downside of header versioning?
It's less discoverable — you can't just paste a URL into a browser to see a given version's response — and caching gets trickier, since the same URL can now return different bodies depending on a header a cache may not vary by default.
In the header-versioning example, what version does a request get if it sends no `api-version` header at all?
Version 1. The handler reads req.headers["api-version"] || "1", so a missing header falls back to the original behavior rather than failing or defaulting to the newest version.
Why does defaulting a missing version header to the original version matter for backward compatibility?
It means clients that were built before versioning even existed keep working exactly as before, with zero changes required on their end, rather than being forced to explicitly opt into 'version 1' to avoid breaking.
What's an alternative to a custom header for expressing an API version, using a standard HTTP mechanism?
Content negotiation via the Accept header, e.g. Accept: application/vnd.myapi.v2+json — the version rides on a mechanism HTTP already defines for negotiating response format, instead of a bespoke header name.
Why doesn't API versioning map cleanly onto semantic versioning (major.minor.patch)?
Semantic versioning tracks every release's granularity, including non-breaking additions; API versions typically only bump when a change is actually breaking for consumers, so a long stretch of purely additive changes might never need a new API version at all.
What is a deprecation window (or sunset period)?
A communicated span of time during which an old API version keeps working exactly as before but is explicitly marked for future removal, giving existing clients time to migrate before it's actually taken away.
How might a server communicate an upcoming version retirement to its clients?
Through mechanisms like a Sunset or Deprecation response header, a warning field in the response body, published changelog/documentation entries, or direct outreach to known API consumers.
Why is silently changing an existing version's behavior worse than releasing a proper new version?
It violates the contract clients trusted was stable, breaking them with no warning and no way to opt out or prepare — whereas a new version lets old behavior keep running untouched while new behavior is opt-in.
Scenario: you need to change a live endpoint's field from a string to a number. What's the safe way to do it?
Don't mutate the existing field's type in place. Either introduce a new version where the change is made, or add a new field alongside the old one and deprecate the old field — either way, existing clients relying on the current type keep working.
Why can adding a new *required* field to a request body be breaking, even though adding fields sounds additive?
Existing clients don't know to send that new required field, so their requests start failing validation the moment it's required — additive is only safe on the response side, where unfamiliar fields can be ignored; on the request side, a new required field is something old clients literally cannot satisfy.
What's the practical risk of letting old API versions pile up indefinitely?
Every version adds ongoing maintenance, testing, and security-patching burden, and more surface area for subtle bugs — the common mistake isn't introducing versions, it's never retiring the ones nobody needs anymore.
Debugging: a client explicitly requesting version 1 via the header is receiving the version 2 response shape. What's a likely bug?
The version comparison itself is probably wrong — e.g. comparing the header value against the wrong type, checking equality against the wrong string, or branching logic ordered so the v2 path runs before the version check happens at all.
Why does API versioning usually require maintaining separate documentation per version?
Documentation has to accurately describe each version's actual contract; docs describing the current version's shape would be actively wrong for a client still calling an older, differently-shaped version.
Trap: a team assumes a breaking change to a GET endpoint is low-risk because 'nobody really uses GET responses directly.' Why is that reasoning unsafe?
Versioning decisions should be based on the resource's actual contract, not assumptions about who's using it — any independently-deployed consumer could be parsing that GET response's exact shape, and there's no way to be sure none are.
How does API versioning differ from a feature flag, given that both can change server behavior for some requests but not others?
A feature flag toggles internal behavior for a codebase you control end-to-end, often for gradual rollout; versioning specifically exists to preserve a stable, documented contract for external or independently-deployed clients that can't simply be forced to flip a flag on your schedule.
Follow-up: once traffic to an old version has dropped to nearly zero, is it safe to delete it immediately?
Not necessarily — 'nearly zero' isn't zero, and a caller can still be quietly relying on it. It's safer to confirm via logs/metrics, announce a firm removal date, and only remove it after that grace period has actually passed.
Advanced: how can an API gateway reduce the internal cost of supporting multiple externally-visible versions?
The gateway can translate or adapt requests/responses for older versions into whatever the current internal implementation actually expects, so the backend itself maintains fewer diverging code paths even while still exposing several versions to the outside world.