·6 min read

REST API Versioning Strategies: URL, Header, and Query Parameter Compared

Compare the three main REST API versioning strategies — URL path, custom header, and query parameter — with trade-offs, examples, and recommendations for each.

Ship a breaking change to a public API and you'll hear about it — from angry tweets, broken integrations, and a support queue that won't stop growing. API versioning is how you evolve your API without breaking the consumers who depend on it.

But there's no single standard. Teams argue endlessly about URL versioning vs header versioning vs query parameters. This guide breaks down each approach with real-world trade-offs so you can pick the one that fits your API.

When Do You Need Versioning?

Not every change requires a version bump. The key distinction is between breaking and non-breaking changes:

  • Non-breaking (additive) — adding a new field to a response, adding a new optional parameter, adding a new endpoint. These are safe to ship without versioning.
  • Breaking — removing or renaming a field, changing a field's type, altering error codes, changing authentication requirements. These need a version boundary.

The goal of versioning is to let you ship breaking changes to new consumers while existing consumers continue using the old contract until they're ready to migrate.

Strategy 1: URL Path Versioning

The most common approach. The version is part of the URL:

GET /v1/users/123
GET /v2/users/123

Pros:

  • Instantly visible — you can see the version in the browser, logs, and docs
  • Easy to route at the infrastructure level (load balancers, API gateways)
  • Simple caching — different URLs, different cache entries
  • Used by Stripe, Twilio, GitHub, and most major APIs

Cons:

  • Breaks REST purists' sensibilities (the version isn't really a "resource")
  • Can lead to duplicated route definitions if not structured carefully
// Express.js — URL path versioning
import { Router } from "express";

const v1 = Router();
v1.get("/users/:id", getUserV1);

const v2 = Router();
v2.get("/users/:id", getUserV2);

app.use("/v1", v1);
app.use("/v2", v2);

Strategy 2: Custom Header Versioning

The version is specified in a request header, keeping URLs clean:

GET /users/123
Accept: application/vnd.myapi.v2+json

# or with a custom header:
GET /users/123
X-API-Version: 2

Pros:

  • URLs remain stable and clean — better for REST purists
  • Supports content negotiation via the Accept header (media type versioning)
  • Used by GitHub (media type) and Azure

Cons:

  • Hidden — you can't see the version in browser URLs or logs without inspecting headers
  • Harder to test casually (can't just paste a URL into a browser)
  • CDN and cache configuration is more complex

Strategy 3: Query Parameter Versioning

The version is a query string parameter:

GET /users/123?version=2

Pros:

  • Easy to add as an afterthought — no routing changes needed
  • Optional with a default — omit the parameter, get the latest (or oldest) version

Cons:

  • Mixes API versioning with business parameters in the query string
  • Easy for clients to forget, leading to subtle bugs when defaults change
  • Less common in practice — may confuse developers who expect path or header versioning

Which Strategy Should You Choose?

For most developer-facing APIs, URL path versioning is the pragmatic choice. It's the most widely understood, the easiest to implement, and the simplest to debug. If you're not sure, start with /v1/ — you can always add header-based versioning later if your use case demands it.

Here's a quick decision matrix:

  • Public API with many consumers? → URL path. Transparency matters.
  • Internal microservices with strict REST compliance? → Header versioning via Accept media types.
  • Retrofitting versioning onto an existing API? → Query parameter as a quick fix, then migrate to URL versioning.

Versioning Tips from the Trenches

  • Version the contract, not the implementation — don't create v2 just because you refactored the backend. Only version when the external contract changes.
  • Deprecate loudly — add Sunset and Deprecation headers to old versions. Log warnings when deprecated versions are called so you can reach out to stragglers.
  • Keep N-1 alive — support at least the previous version. Two concurrent versions is manageable; five is a maintenance nightmare.
  • Document migration paths — for every breaking change, publish a migration guide showing the before/after. Your consumers will thank you.
  • Use date-based versions for rapid iteration — Stripe uses versions like 2024-12-01 instead of v1, v2. This works well when you ship small breaking changes frequently.

How API Snap Handles Versioning

API Snap takes a stability-first approach: the API surface is designed to be additive, so breaking changes are rare. Endpoints use clean, unversioned URLs like /api/qr, /api/screenshot, and /api/hash. New capabilities are added as optional parameters rather than breaking changes — so you can build on the QR Code API, Screenshot API, or any other endpoint without worrying about version churn.

Get Started

Designing a new API? Start with /v1/ in your URL structure and commit to backward-compatible changes as the default. If you want to see a well-designed API surface in action, create a free API Snap account and explore the API documentation — or try endpoints directly in the playground.

Ready to try it?

Get your free API key and start building in under a minute.