DEV Community

Jeff
Jeff

Posted on Originally published at powerduck.com

Mocking API Version Negotiation: Headers, Paths, Fallbacks

Quick answer: A client declares the API version it wants, through a versioned path, a header, or a query value, and the server picks the matching contract or applies a documented fallback. Mock this by matching the version declaration with a conditional response for each branch, including the default, deprecated, and unsupported cases. This lets you test how the client handles a changed schema, a deprecation warning, and an upgrade error without keeping a real old version running.

How a client declares a version

There are four common mechanisms, and many APIs use one consistently. A versioned path such as /v1/resources and /v2/resources makes the version obvious in the route. A dedicated header such as X-API-Version: 2 keeps the URL stable. A vendor media type such as Accept: application/vnd.example.v2+json carries the version inside content negotiation, and a query parameter such as ?version=2 is simple but easier to leak into links and caches. Whichever one the API uses, the server has to answer three questions: what to do when a version is explicit, what to return when none is given, and how to reject a version that is no longer supported.

Mock each negotiation branch

Use conditional overrides that match the version declaration and return a response that genuinely differs between versions, since identical bodies cannot prove the client handles a schema change. Cover the branches below.

Client sends Server choice Response Client expectation
Version 2 explicit v2 contract 200, v2 schema Parse the new fields
Version 1 explicit v1, deprecated 200 plus deprecation headers Warn, keep working
No version Documented default Default schema Stable, predictable behavior
Unsupported version None match 400 or 404 Show an upgrade message
Old versioned path Routed v1 v1 response Route-specific handling

The deprecated row should include the deprecation and sunset headers a real API emits, so the client can surface a warning without treating the response as a failure. The unsupported row is the one that catches a client blindly parsing an error body as data.

What the client must handle

Set the version declaration in one place rather than scattering it across requests, so a change does not leave some calls on the old contract. On a deprecated version, read the warning headers and log or surface them while continuing to parse the response. On an unsupported version, treat the status as an error and show the upgrade path instead of retrying the same call. Where v1 and v2 differ in fields or types, keep the parsing logic tied to the version returned so the client does not assume every response has the new shape. Inspect the recorded requests to confirm the version marker the client actually sent. A desktop API workspace lets you match these version branches with conditional overrides, inspect the recorded requests, and assert on each response in the same place. Powerduck works this way, so negotiation is a fixed set of local responses.

Honest limits

A local mock runs in the desktop main process and does not replace the real backend, so the version map, default, and fallback rules are ones you define. The mock does not model real gateway routing, traffic splitting, or canary rollout, and it does not prove the live service keeps old versions available or retires them on the same schedule. Confirm the real default version, deprecation headers, and unsupported-version status with a small set of genuine requests. Importing a schema does not deploy a backend, and a passing mock run is not evidence about the production version lifecycle.

Frequently asked questions

Is version negotiation the same as content negotiation? No. Content negotiation selects a representation such as JSON or XML through Accept. Version negotiation selects a contract such as v1 or v2, even when a vendor media type carries the version.

What should happen when no version is sent? The server should return a documented default version rather than guessing. Mock that branch so the client relies on a predictable response.

How do I test an unsupported version without an old server? Mock the rejection as a 400 or 404 and assert the client shows an upgrade message instead of parsing the error body.

Should a deprecated version still work? Usually yes, while returning deprecation and sunset headers. The client should warn and keep working, then migrate before the sunset date.

The bottom line

Treat version negotiation as a defined set of branches, not an implicit default, and mock each one with responses that actually differ between versions. Cover the explicit, default, deprecated, and unsupported cases, keep the version declaration centralized, and verify the real gateway and lifecycle separately. For more on this approach, see the API mocking guide hub.

Top comments (0)