Every versioning scheme has zealots, and most of the argument is theology. When I had to choose one for a public, cache-heavy API, the deciding factors were dull and concrete: could a CDN cache it, could a customer read it, and could I run two versions side by side without the router turning into a maze.
The three realistic options are URL path versioning (/v1/orders), custom-header versioning (a header like X-Api-Version: 2), and media-type versioning (Accept: application/vnd.acme.v2+json). All three work. The REST purists will tell you the URI should identify a resource, not a representation, and that versioning therefore belongs in a header. They are right in the abstract and I ignored them anyway, because a public API is a product with human users, and the axes that decide the outcome are cacheability, discoverability, router simplicity and how gracefully you can run two versions in parallel. On those axes, path versioning won for this API. Here is the reasoning, including where it does not win.
Cacheability decided it faster than anything else
This API sits behind a CDN and an nginx cache, and a large share of traffic is anonymous reads. That single fact does most of the work. A cache key is built from the request URL. With path versioning, /v1/catalog and /v2/catalog are two different URLs and therefore two different cache entries, automatically, with no configuration and no surprises.
Free · 4 minutes
Is your engineering team shipping safely, or quietly accumulating risk?
Fourteen questions on how work gets from idea to production — cadence, testing, rollback, and the key-person risk in your delivery. Banded finding on screen, full sheet by email.
Version-in-a-header breaks that model unless you are careful. If the response body differs by the value of Accept or a custom version header, the cache must key on that header too, which means emitting Vary: Accept (or Vary on your custom header) and trusting every layer between you and the client to honour it. Many CDNs treat a broad Vary: Accept as close to uncacheable because the header space is large and messy. Custom headers are worse: intermediary caches ignore headers they do not understand by default, so you can silently serve a v1 body to a client that asked for v2. That is not a hypothetical edge case; it is the normal failure mode of header versioning in front of a cache you do not fully control. Path versioning has none of this. The URL is the version, the cache already keys on the URL, and there is nothing to get wrong.
Discoverability is a support-cost decision
A version you can see in a URL is a version a developer can paste into a browser, a curl line, a bug report or a Slack message, and everyone looking at it knows exactly which version produced it. That is worth more than it sounds. Header versioning hides the version in request metadata, so a customer’s “it returns the wrong field” ticket arrives without the one piece of information you need, and you spend the first reply asking which version they are on.
Media-type versioning is the most correct and the least discoverable of the three. Asking an integrator to send Accept: application/vnd.acme.v2+json to get anything back is a real barrier for the person wiring up their first request from a scripting language, and it makes copy-paste examples longer and more fragile. For an internal API consumed by teams who read the spec, that cost is tolerable. For a public API whose adoption depends on a five-minute quickstart working on the first try, it is a tax on your own growth. The same instinct that makes a well-prefixed API key easy to recognise on sight applies to versions: make the important thing visible.
Router simplicity and running two versions side by side
Path versioning gives you the simplest possible routing story. /v1/ and /v2/ are separate location blocks in nginx, and you can point them at entirely separate upstreams: different services, different deployments, even different codebases, with no shared routing logic that has to inspect a header and branch. When you eventually retire v1, you delete a location block and return a clean 410 Gone from that prefix. Header-based routing forces every proxy layer to read and switch on a header value, which is more configuration, more places to introduce a bug, and a harder thing to reason about when you are debugging at two in the morning.
Running two major versions in parallel is where a public API actually lives, because you cannot force thousands of integrators to migrate on your schedule. Separate paths make the parallelism explicit and boring. Each version has its own OpenAPI 3.1 document, its own generated client, its own deprecation timeline. There is no ambiguity about which contract a given request is exercising, which matters when a customer disputes behaviour and you need to point at the exact document that governed their call. Treating each version cutover as a deliberate, measured change rather than a silent header flip is the whole point of governing change with real signals.
When header versioning genuinely wins
I am not claiming path versioning is universally correct, and it is worth being honest about where headers are the better tool. GitHub versions its REST API with a date-valued X-GitHub-Api-Version header (the widely used value is 2022-11-28), and Stripe versions with a date-based Stripe-Version header. Both are deliberate, and both are instructive.
Header versioning wins when the version axis is fine-grained and frequent, as date-based schemes are, because minting a fresh URL namespace for every dated change would be absurd. It wins when the API is not fronted by a shared cache you have to please, or when responses are authenticated and per-user anyway so caching is off the table. It wins when the consumers are a controlled set of well-behaved clients that always send the header, rather than the open internet. And it keeps the URI “pure” in the REST sense, which is a real value if your consumers care about it. Stripe’s model in particular pins each account to a version and lets responses evolve underneath a stable header, which is a sophisticated answer to a problem coarse path versioning does not try to solve.
The cheapest version is the one you never cut
The best versioning strategy is to need it as rarely as possible. Most changes do not require a new version at all: adding a field, adding an endpoint, adding an optional parameter, accepting a new enum value your clients can ignore. If you only ever add and never remove or repurpose, a well-behaved client written against v1 keeps working indefinitely, and you spend a new major version only on the genuinely breaking changes, removing a field, renaming one, tightening validation, changing a default. Design clients to tolerate unknown fields and additive change, the same additive discipline that makes a POST safe to retry, and the version boundary becomes a rare, deliberate event rather than a monthly chore.
So the honest answer to why I picked /v1/ is not that it is theoretically superior. It is that for a public, cache-heavy API served through a CDN and nginx, path versioning made the cache work for free, made the version visible to the humans who file the tickets, and kept the router boring. Pick the scheme that removes your most expensive failure mode. For a private, authenticated, rapidly evolving API, that answer is a header. For a public one you want the internet to cache and adopt, it is the path.
Build and rescue work
Hands-on delivery of this kind is handled by Sixteen Pillars Studio.
Free interactive tool
Website compliance checklist
What your site has to do, based on what it actually does
Answer as much or as little as you like — the list builds as you go. Nothing is stored against your name and no email is required.
Everything that applies
Ordered by what to do first: legal requirements you can close quickly, then larger pieces of work, then what is expected rather than required. Not exhaustive, and not a legal audit.
Dated PDF, yours to keep or circulate.
Can you trust the architecture you have?
Architecture diagrams rarely show the reality of how systems actually operate. An independent review establishes what is really there.