If your API is going to live for two years, you need to plan versioning on day one. Otherwise 18 months in you’re stuck saying “I can’t ship a breaking change, it’ll break clients”.
Three main approaches. Let’s compare what the theory says with what happens in production.
1. URL Path Versioning
GET /api/v1/users and GET /api/v2/users
This is common and practical. Stripe, GitHub, Twitter, they all use this (or something like it).
What theory says
“URLs should identify resources. A version is not a resource. This violates REST.”
What happens in production
- It works. You can open it in a browser, test it in Postman, docs are clear.
- Router config is trivial:
/api/v1/*→ v1 handler,/api/v2/*→ v2 handler. - Monitoring and analytics are easy: you can see exactly how much each version is used, straight from the logs.
- Natural in client libraries: the Stripe SDK calls
stripe.users(), the URL underneath can change without affecting developer experience.
The counterargument is about REST purity. But if you expect API purity to solve your practical problems, you’ll run into trouble on other fronts too.
2. Header-based Versioning
GET /api/users + Accept-Version: v2 header
What theory says
“The URL stays clean. Resource identity is independent of the version.”
What happens in production
- Debugging is painful: you can’t just open a URL in a browser, headers don’t show up in default log output
- Caching is a problem: CDNs and reverse proxies have to cache different versions separately, and misconfiguration muddles them
- Documentation ends up stuffed with “you must send this header” warnings
- Client libraries need extra configuration (setting a default header)
I’ve started with header-based versioning on two projects, and both times we moved to URL path after a year to eighteen months. Every time the reason was the same: debugging pain and client-side issues.
My take: avoid header-based versioning unless you’re running a genuinely large API platform.
3. Content Negotiation (Media Type Versioning)
GET /api/users + Accept: application/vnd.mycompany.v2+json
What theory says
“The most RESTful approach. Content type describes the response format.”
What happens in production
- All the header-based pain, plus more complexity
- The client library has to set the Accept header correctly
- Most HTTP clients don’t touch this, and developers get confused
- Swagger and OpenAPI tools don’t play nicely with it
I never recommend it. The theoretical upside doesn’t justify the practical overhead.
Which one, when?
Practical rule:
- 90% of projects: URL path (v1, v2). Easy, clean, developer-friendly.
- Internal API + a strong operations team: header-based is possible, but not necessary.
- Content negotiation: skip it unless you’re writing an academic paper.
When do you bump the version?
A more important question. What counts as a “breaking change”?
Breaking change (requires v2):
- Removing or renaming a response field
- Making a request field required (it was optional before)
- Deleting an endpoint
- Changing the authentication method
Non-breaking (stays on v1):
- Adding a new optional response field
- Adding a new endpoint
- Adding a new optional request parameter
- Changing response ordering (clients shouldn’t depend on order)
The line isn’t crisp, there’s a gray area. Stripe’s approach is good: every small API change gets a new “API version” (date-based: 2023-10-01), the client declares which date it pinned to via an Accept-Version header. Elegant, but the implementation overhead is high.
Deprecation strategy: the other half of versioning
Versioning isn’t just opening new versions, it’s being able to close old ones. Otherwise you end up supporting four versions in parallel, all of them maintenance load.
My approach:
- Announcement (6 months out): blog post + email + response header
Deprecation: true+Sunset: 2026-06-01 - Warnings (3 months out): stronger warnings in response headers, email reminders
- Graceful degradation (1 month out): the old API still works, but the rate limit drops
- Shutdown: 410 Gone, single message “upgrade to v2”
This 6-month window is enough for enterprise clients to migrate. Three months is too short, a year is overkill.
Monitoring: which version gets used how much?
Log the version on every API request. Weekly dashboard:
- v1 usage: 35%, trending down ✓
- v2 usage: 65%, trending up ✓
If v1 usage isn’t coming down, your deprecation messaging needs to get louder. If v2 adoption is fast, you can pull the v1 shutdown in.
You can’t decide without metrics, “I think nobody’s on v1 anymore” is not real data.
Takeaway
Make the versioning decision on day one. URL path is simple, practical, developer-friendly, and the right first choice for most projects.
Define “breaking change” clearly, plan the deprecation timeline deliberately, and don’t skip monitoring. Get those three right and versioning isn’t pain, it’s a maintenance routine.