We broke backward compatibility twice before getting our versioning right. Here's the approach we've stuck with for two years.
We've broken our API twice. Both times were painful. Both times were avoidable. Here's the versioning strategy we've used for the past two years that has kept us break-free.
What we did wrong in v1
No versioning at all. Resources lived at /api/tasks, not /api/v1/tasks. When we needed to change a response shape, we had to choose between breaking customers or maintaining two incompatible code paths indefinitely. We chose to break customers. They were not happy.
What we did in v2
URL versioning (/api/v2/). Better, but we still treated minor changes as non-breaking when they weren't. Removing a field is a breaking change even if you're just "cleaning up." We learned this the hard way.
Our current approach
We use URL versioning with a strict deprecation policy:
- New version for any breaking change (field removal, type change, auth change)
- Old version supported for 12 months minimum after deprecation notice
- Sunset headers on deprecated endpoints so clients can detect and act
- Automated tests that run the v(n-1) test suite against the current API to catch unintended regressions
It's not glamorous. But it's reliable, and our customers trust it.