How to Design API Versioning Without Breaking Clients
API versioning is less about /v1 vs /v2 and more about contracts. Here’s a practical approach that keeps old clients working while you ship new features.
Author

Shipping an API is easy. Keeping it usable for years is the hard part. Versioning is how you change shape without stranding the apps already calling you.
Start with a contract, not a number Before you add /v2, write down what clients can rely on: required fields, error shapes, auth headers, and pagination. If that contract is clear, most “breaking” changes become deliberate choices instead of accidents.
Prefer additive changes Add optional fields. Keep old fields. Introduce new endpoints for new workflows. Deprecate loudly, remove slowly. Most teams don’t need a major version for every improvement—they need discipline.
• Add, don’t rename • Never reuse a field for a new meaning • Document deprecations with a removal date
When you do need a new version Use a new version when the response shape or auth model changes in a way old clients cannot ignore. Put the version in the URL (/api/v2/...) or in a header—pick one and stick with it. URL versioning is usually easier for debugging and caching.
app.enableVersioning({ type: VersioningType.URI });
@Controller({ path: 'posts', version: '1' })
export class PostsController {
@Get('mine')
findMine(@CurrentUser() user: AuthUser) {
return this.postsService.findMine(user);
}
}That serves GET /api/v1/posts/mine. A breaking shape gets a new controller with version: '2', and v1 stays until clients move.
A simple rollout plan
• Ship v2 behind feature flags or a separate path • Migrate your own clients first • Give external clients a sunset window • Monitor 4xx rates and usage by version • Only then turn off v1
What to avoid Don’t silently change types. Don’t remove fields without a deprecation period. Don’t invent a new error format per endpoint. Consistency matters more than cleverness.
Bottom line: version when the contract breaks. Everything else can ship as additive, documented change.
Write the contract down
A version number does not tell a client what is safe to depend on. Before you publish /v1, list the promises in one place: which fields are required, which are optional, how errors are shaped, which headers carry auth, and how pagination works. When someone proposes a change, the question is whether that list still holds. If it does, you do not need a new version. If it does not, you do.
Put that list where the people who ship the server and the people who call it will both see it. A changelog that only the server team reads will not stop a client from breaking on a Tuesday.
Additive changes in practice
An additive change is one an old client can ignore. A new optional JSON field is additive. A new query parameter with a default that preserves today’s behavior is additive. A new endpoint that old clients never call is additive. Renaming user_id to userId is not. Changing a number that used to be a string is not. Returning 200 for a case that used to return 404 is not, if any client branches on the status.
When you deprecate a field, keep returning it. Name the replacement, name the date you will stop, and keep a count of callers that still depend on the old field. Remove it only after that date, and only once the count says the remaining callers are ones you can migrate yourself.
Pick one version signal
URL versions such as /api/v1/posts are easy to log, cache, and paste into a bug report. A header, whether Accept or a custom version header, stays out of the path but is easier to drop in a gateway, a CDN, or a generated client. Use one. Document it next to the contract. Do not accept the version in the URL and in a header and then let the two disagree.
Nest’s URI versioning, as in the controller above, makes the version part of the route. A second controller with version 2 can serve the new shape while version 1 keeps the old one. That split is the point of a major version: two contracts, two implementations, and one retirement date.
Retire the old version on purpose
A sunset is a date, a metric, and a message. Tell clients in the docs and, when you can, in a response header. Watch calls per version, not just overall traffic. A version that is one percent of requests can still be a payroll integration. Move your own apps first so you feel the new contract before you ask anyone else to. When the date arrives, return a clear error that names the replacement, not an empty 404.
Version the contract when it breaks. Ship everything else as an addition the old client can skip.