Which API Versioning Strategy Should You Choose? URI vs Header vs Media Type
When you evolve a REST API, you face a problem: how do you add new features without breaking existing clients? You might need to remove a field, rename an endpoint, or change how resources relate to each other. These breaking changes require versioning, but which strategy should you choose?
I think the answer depends on your priorities. Let me walk you through the four main API versioning strategies and their trade-offs.
The Four Versioning Strategies
URI Versioning
The simplest approach puts the version directly in the path:
GET https://api.contoso.com/v2/customers/3This works well because it’s explicit. Anyone looking at the URL immediately knows which version they’re using. Caching is straightforward too—each version has its own URI, so the same URL always returns the same data.
The downside? The same customer (customer 3) now has different URIs across versions. This violates a REST principle that a resource should have a single canonical identifier. When you build HATEOAS links, you must include the version in every URL.
Query String Versioning
You can also pass the version as a query parameter:
GET https://api.contoso.com/customers/3?version=2This keeps the resource URI consistent—the customer 3 is always /customers/3. For clients that don’t specify a version, you can default to version 1.
But here’s the problem: some older browsers and web proxies don’t cache responses for requests that include query strings. This creates unexpected caching behavior. When you implement HATEOAS, you also need to include the version in every link, which complicates the implementation.
Header Versioning
Header versioning moves the version out of the URL entirely:
GET https://api.contoso.com/customers/3Custom-Header: api-version=2Now your URIs are clean—customer 3 always has the same identifier. You can provide default behavior for clients that omit the header. HATEOAS links don’t need version information embedded in them.
However, this approach requires a Layer 7 gateway that can inspect headers. Not all infrastructure supports this. You also need to handle the logic of parsing custom headers on every request.
Media Type Versioning
Media type versioning uses the Accept header to specify the version:
GET https://api.contoso.com/customers/3Accept: application/vnd.contoso.v2+jsonThe server responds with the matching Content-Type:
Content-Type: application/vnd.contoso.v2+json
{"id":3,"name":"Fabrikam","address":{"streetAddress":"1 Microsoft Way","city":"Redmond"}}This approach uses standard HTTP mechanisms. When you implement HATEOAS, you can include the MIME type in resource links, making this strategy well-suited for hypermedia APIs.
The trade-off is complexity. You need to define custom media types, parse Accept headers on the server side, and handle cache fragmentation across different versions.
Comparison Table
+----------------------+-------------------+----------------+-----------------+-----------------------+| Strategy | Cache Compatible | URI Cleanliness| HATEOAS Support | Implementation Cost |+----------------------+-------------------+----------------+-----------------+-----------------------+| URI Versioning | Excellent | Poor | Poor | Low || Query String | Moderate | Good | Poor | Low || Header Versioning | Moderate | Excellent | Good | Moderate || Media Type Versioning| Moderate | Excellent | Excellent | High |+----------------------+-------------------+----------------+-----------------+-----------------------+Which Should You Choose?
For public APIs, I recommend URI versioning. Stripe, GitHub, and most major API providers use it because it’s simple and cache-friendly. Your users can easily see which version they’re using, and your caching infrastructure works without special configuration.
For internal APIs that require HATEOAS, consider header or media type versioning. These keep your resource URIs clean and work well when you need to embed versioning information in hypermedia links.
Avoid query string versioning unless you have a specific reason to use it. The caching problems create headaches that surface at the worst times—when you’re scaling or migrating to new infrastructure.
Common Mistakes
I’ve seen teams choose header versioning without realizing their load balancer can’t inspect Layer 7 headers. They end up building custom routing logic that becomes a maintenance burden.
Another mistake is not providing default behavior when clients omit version information. Your API should have a sensible default—usually the oldest supported version—so older clients continue working during transitions.
The biggest mistake I see is ignoring HATEOAS complexity when choosing a strategy. If you plan to build hypermedia-driven APIs, pick a versioning approach that works well with embedded links from the start. Changing versioning strategies later is painful.
Summary
In this post, I compared four API versioning strategies and explained when to use each one. URI versioning works best for public APIs due to its simplicity and cache compatibility. Header and media type versioning suit internal APIs that need clean URIs and HATEOAS support. Query string versioning creates caching problems that make it a poor choice for most scenarios. Choose your strategy based on whether you prioritize simplicity, cache behavior, or hypermedia capabilities.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments