Compare the main ways to version an HTTP API — a version in the URI path such as /v1/orders, a query parameter, a custom request header, and media-type versioning via the Accept header. What are the trade-offs?
answer
- path = visible, routable, cache-safe, coarse
- query param = easy to lose, muddies caching
- custom header = clean URIs, needs Vary, invisible
- media type = content negotiation done right, 406
- never default to latest
basics
~20 sURI path versioning is the most visible, cacheable and easiest to route or test in a browser, but versions the whole API. Header and media-type versioning keep URIs stable and allow per-resource versions, at the cost of discoverability, tooling friction and cache correctness needing Vary.
solid answer
~50 s**URI path** (`/v1/orders`): visible, trivially routable at the gateway, easy to curl or paste in a browser, unambiguously cacheable. Downsides: a resource has two URIs, which is philosophically ugly and practically annoying for links and identity, and teams usually end up bumping the whole API at once. It dominates in practice because operations are simple. **Query parameter** (`?version=2`): similar reach, easy to add, but easy to drop accidentally, muddles caching and mixes contract selection with resource filtering. **Custom header** (`X-API-Version: 2`): keeps URIs clean and allows per-resource versions, but is invisible in a browser, awkward in curl and docs, and requires `Vary` on the header for correct caching. **Media type** (`Accept: application/vnd.github+json; version=3`): the purist option — the URI identifies the resource, the representation is negotiated. Strong for per-resource evolution, but the most tooling friction and the least understood by client developers. Pick one, apply it everywhere, and document defaults.
code
http · 10 linesGET /v1/orders/42 HTTP/1.1
Host: api.example.com
GET /orders/42 HTTP/1.1
Host: api.example.com
X-API-Version: 2
GET /orders/42 HTTP/1.1
Host: api.example.com
Accept: application/vnd.example+json; version=2go deeper
Name the options and give the main pro and con of each; know /v1/ is the common default.
Compare cacheability, routing, tooling and granularity, and explain the Vary requirement and the unversioned-request default.
Argue the choice from operations — gateway routing, observability, cache behaviour, support burden — and insist on one scheme platform-wide.
Decide the organisational policy: granularity of versioning, default policy, how many versions are supported at once, and the carrying cost each implies.
## What all four have in common Every scheme answers one question: *how does the caller select which contract they want?* The differences are operational — visibility, cacheability, routing, tooling — rather than deep. ## URI path versioning `GET /v1/orders/42`. Strengths: the version is visible in every log line, trace, curl command and support ticket. Gateways route on prefix, so v1 and v2 can be separate deployments with independent scaling and rollback. Caches key on the URL, so correctness is automatic. A developer can paste it into a browser. Docs and SDKs are unambiguous. Weaknesses: the same underlying entity now has two identifiers, which complicates hypermedia links, stored URLs and cross-references. Because the prefix is coarse, teams bump the entire API for a change affecting one endpoint, forcing customers to migrate code they had no reason to touch. ## Query parameter versioning `GET /orders/42?version=2`. Easy to add, still visible and browser-friendly. But it is easy to omit — which makes the default version load-bearing — it conflates contract selection with resource filtering, and shared cache keys depend on parameter normalisation. Rarely a first choice, common as a retrofit. ## Custom request header `X-API-Version: 2` (or a vendor-specific name). URIs stay canonical: one resource, one URL. Versions can be per-resource, so a change to orders does not force a bump for payments. Costs: invisible in browsers and most logs unless deliberately captured; every curl example gets longer; a forgotten header silently selects the default; and shared caches must be told to `Vary` on that header or they will serve one version's body to another version's caller. It also breaks HTTP's own conventions less cleanly than content negotiation, since HTTP already has a mechanism for this. ## Media-type versioning `Accept: application/vnd.github+json; version=3` or `application/vnd.example.order.v2+json`. This is content negotiation used as designed: the URI names the resource, the media type names the representation. Requests use `Content-Type` symmetrically, and the server should reply with the media type it actually produced. Non-negotiable requests yield **406 Not Acceptable**. `Vary: Accept` handles caching — and since caches already vary on `Accept` far more reliably than on custom headers, this is the safest header-based option operationally. Strengths: per-representation granularity, no URI duplication, standards-aligned. Weaknesses: unfamiliarity — many client developers have never set a vendor media type; tooling like browsers and simple HTTP clients default to `*/*`; parsing suffixes and parameters correctly on the server takes care; and documentation is heavier. ## Defaults, and choosing Whatever the mechanism, define what happens when the caller specifies nothing. Two defensible policies: **pin to the oldest supported version** (existing integrations keep working, new ones must opt in) or **reject unversioned requests** (explicit, noisier onboarding). Defaulting to *latest* is the one clearly bad option — every release breaks every lazy client. In practice: URI path for most public APIs where operability and developer familiarity dominate; media-type or header versioning where per-resource evolution and URI stability genuinely matter. The costly mistake is mixing schemes across teams, so the same platform is versioned three ways.
- Why does header or media-type versioning require the `Vary` response header?Shared caches key stored responses on the URL. If two versions share a URL and differ only by a request header, a cache can serve a v1 body to a v2 caller. `Vary: Accept` or `Vary: X-API-Version` tells caches to include that header in the key. Caches handle `Vary: Accept` far more reliably than arbitrary custom headers, which is an argument for media-type versioning over a bespoke header.
- What should the server do when a request specifies no version at all?Apply a documented default. Pinning to the oldest supported version keeps existing integrations working and forces new consumers to opt into newer contracts; rejecting unversioned requests with a 400 is stricter and unambiguous. Defaulting to the latest version is the harmful choice, because every release silently breaks callers who never asked to move.
saying these in an interview costs you the question
- Claiming URI versioning is objectively wrong on REST-purity grounds without weighing operability
- Using a custom version header without setting `Vary`, so shared caches mix versions
- Defaulting unversioned requests to the newest version
- Mixing several versioning schemes across teams in one platform
- Assuming a `/v2/` prefix means every endpoint under it actually changed