API Versioning: How to Change an API Without Breaking Clients
Once other apps depend on your API, changes can break them. What counts as a breaking change, versioning strategies (URL path, headers, Stripe-style dated versions), evolving without versions, deprecation with Sunset headers, and a practical approach for small teams.
While only your own front end uses your API, you can change it freely and deploy both together. Once mobile apps (which users don't update immediately), customers' integrations or partners depend on it, a change can break code you don't control. Versioning is how you evolve an API without doing that. (What is an API?)
What's breaking and what isn't
Breaking changes (need a new version or careful migration):
- removing or renaming a field or endpoint
- changing a field's type (
"42"→42) or meaning - adding a required request parameter
- changing error codes or formats clients rely on
- changing authentication
- tightening validation that used to accept something
Non-breaking (usually safe):
- adding a new endpoint
- adding an optional request parameter
- adding a field to a response (if clients ignore unknown fields — tell them to)
- adding a new enum value — borderline; strict clients may break
The safest API is one that mostly evolves by adding.
Strategies
1. Version in the URL
GET /v1/invoices
GET /v2/invoices
- ✅ Obvious, easy to route, easy to test in a browser, easy to see in logs.
- ❌ Whole-API versions; tempting to fork everything for one change.
The most common choice, and a good default.
2. Version in a header
GET /invoices
Accept: application/vnd.acme.v2+json
# or
Api-Version: 2
- ✅ Clean URLs; can version per resource.
- ❌ Less visible, harder to try by hand, easy for caches to mishandle (add
Vary).
3. Dated versions (the Stripe model)
Each account is pinned to the API version (a date) it started with. Clients can send a header to use a newer version. Internally, the server runs the latest code and applies transformations to responses for older versions.
- ✅ Clients never break unexpectedly; small, frequent changes possible.
- ❌ Significant engineering to maintain the transformation layers.
Great for large public APIs; heavy for a small team.
4. No versions — evolve carefully
Many internal and small APIs never version: add fields, never remove, deprecate gently. GraphQL APIs typically work this way. (GraphQL vs REST)
Deprecating properly
When something must go:
- Announce it — changelog, email to API users, docs.
- Signal it in responses: the
Deprecationheader and aSunsetheader with the removal date, plus aLinkto migration docs. - Watch usage — log which clients still call the old version or field. (Structured logging)
- Contact the stragglers before the date.
- Remove it — and keep the old version's tests until you do.
Give generous notice: months for public APIs.
Document the contract
An OpenAPI description per version makes changes reviewable — a diff of the spec shows breaking changes before they ship, and tools can check this automatically in CI. (OpenAPI vs Swagger)
Mobile apps need extra care
Old app versions live on phones for years. Options: keep old endpoints working, have the API tell outdated apps to upgrade (a minimum-version check), and never assume everyone has the latest build. (Web app vs mobile app)
A practical approach for small teams
- Prefix routes with
/v1/from day one — it costs nothing and leaves room. - Evolve within v1 by adding whenever possible.
- Treat the response shape as a contract; test it. (How to test an API)
- Only create v2 for a genuinely different design, and run both until v1 traffic is gone.
- Same rule applies to webhooks you send: version the payload. (Idempotency keys)
EasySpawn runs your API with logs you can query, so you can see exactly which clients still call v1 before you retire it — and Claude Code can write the migration guide. See how it works or join the waitlist.
Related: Designing a REST API · OpenAPI vs Swagger · tRPC vs REST · API Authentication Methods
Keep reading
tRPC vs REST: End-to-End Type Safety or a Standard API?
tRPC lets a TypeScript front end call back-end functions with full type safety and no API schema to maintain; REST is the universal, language-agnostic standard. How they differ, a tRPC example, when tRPC shines (TypeScript monorepos), when REST wins (public APIs, mobile, other languages), and where Server Actions fit.
TanStack Query vs useEffect for Data Fetching in React
Fetching in useEffect looks simple until you need loading states, errors, caching, race conditions, refetching and mutations. What TanStack Query handles for you, side-by-side code, mutations with invalidation, and when server components or plain useEffect are still the right choice.