Editorial Note: API versioning is one of those things every team knows they should do but most defer until it is too late. Michael Sun lays out the real trade-offs between versioning strategies and argues — convincingly — that the cost of starting with versioning is far less than the cost of retrofitting it.
The API Versioning Conversation You Should Have Had Six Months Ago
Here is a scenario I have watched play out at least a dozen times. A team builds an API. It starts as an internal tool, then a mobile app consumes it, then a partner integration, then a public developer program. Somewhere around the third consumer, someone changes a response field name and an entire mobile release breaks in production.
The post-mortem always reaches the same conclusion: we should have versioned our API from the beginning. And then someone asks “how?” and the real arguments start.
API versioning is not a solved problem. There is no universally correct approach. But the decision to version — and the strategy you choose — has massive downstream consequences. Getting it right on day one is cheap. Retrofitting it later is expensive and painful.
Why Versioning From Day One Matters
The argument against early versioning usually sounds like this: “We only have one client. We can change anything. Versioning adds unnecessary complexity.” This reasoning fails for two reasons.
First, your API contract is a promise, even if the only consumer is your own frontend. Once you ship a client that depends on a specific response shape, changing that shape requires coordinating the API change with a client deployment. This is versioning — you are just doing it implicitly and dangerously.
Second, adding versioning to an existing API with active consumers is dramatically harder than building it in from the start. You have to audit every endpoint, identify every consumer, build a compatibility layer, migrate clients, and maintain multiple versions simultaneously — all without downtime. Starting with versioning means none of this pain ever materializes.
The Three Main Strategies
URL Path Versioning
This is the most visible and most common approach. Your API endpoints include the version directly in the URL:
GET /api/v1/users/123
GET /api/v2/users/123
Advantages: It is immediately obvious which version a client is using. Caching works naturally because different versions have different URLs. Routing is straightforward — your web server or API gateway can direct v1 and v2 traffic to different backend services if needed. Documentation is clear because each version has its own set of endpoints.
Disadvantages: URL path versioning violates the REST principle that a URL should identify a resource, not a representation. /v1/users/123 and /v2/users/123 are the same user — the version describes how the response is formatted, not which resource is being accessed. This is a theoretical concern that rarely matters in practice, but it offends purists.
The more practical problem is that URL versioning encourages coarse-grained version bumps. When the version is in the URL, teams tend to create a new version for the entire API rather than versioning individual endpoints. This leads to “big bang” version transitions where v2 is a complete replacement for v1, which is expensive to build and expensive for clients to migrate to.
When to use it: Public APIs where clarity and simplicity matter most. APIs consumed by third-party developers who need to easily understand and control which version they are using. This is the strategy I recommend for most teams starting out.
Header-Based Versioning
With this approach, the version is specified in a request header, typically a custom header or the standard Accept header with a vendor media type:
GET /api/users/123
Accept: application/vnd.myapi.v2+json
# or with a custom header:
GET /api/users/123
X-API-Version: 2
Advantages: URLs remain clean and RESTfully correct — they identify resources, and the version describes the representation. This approach supports fine-grained versioning because individual endpoints can respond to version headers independently. It also allows for content negotiation, where the server can return the best available version based on the client’s capabilities.
Disadvantages: The version is invisible in the URL, which makes debugging harder. When someone pastes an API URL into a browser or a Slack message, you cannot tell which version they were hitting. Caching becomes more complex because CDNs and proxy caches need to be configured to vary on the version header. API testing with simple tools like curl requires remembering to include the header every time.
The biggest practical issue is that header versioning requires more sophisticated client libraries. Every HTTP request needs the version header attached, which means your SDK or client code needs a reliable way to set default headers. This is trivial in well-structured code but becomes a source of bugs in quick scripts and one-off integrations.
When to use it: APIs where RESTful purity matters, where you need fine-grained per-endpoint versioning, or where the API consumers are primarily using well-maintained client libraries rather than making ad-hoc HTTP requests.
Query Parameter Versioning
The version is passed as a query parameter:
GET /api/users/123?version=2
Advantages: The version is visible in the URL but does not change the path structure. It is easy to add to existing APIs without restructuring routes. Default behavior can be set server-side, so clients that omit the parameter get the current stable version.
Disadvantages: Query parameters are typically associated with filtering and pagination, not API metadata. This creates a conceptual mixing that can confuse developers. Caching behavior varies — some CDNs strip query parameters, some cache different parameter combinations as separate entries. And optional query parameters mean you can never be sure which version a client intended to use if they forget the parameter.
When to use it: As a secondary versioning mechanism or for APIs that need backward-compatible version testing. I rarely recommend this as a primary strategy because the ambiguity of optional parameters creates more problems than it solves.
The Strategy I Actually Recommend
For most teams, especially those building their first production API, I recommend URL path versioning with a specific set of constraints:
- Start with /api/v1/ from day one. Even if you never create v2, the cost is zero and the optionality is valuable.
- Treat versions as major milestones, not minor changes. Non-breaking changes — adding new fields, new endpoints, new optional parameters — do not require a new version. Only breaking changes — removing fields, changing types, restructuring responses — warrant a version bump.
- Define what “breaking” means in writing. Create an API compatibility policy that specifies exactly what changes are considered breaking. Publish it. Hold your team to it. This document prevents arguments later.
- Support at most two versions simultaneously. When v3 launches, v1 gets a deprecation timeline. Supporting more than two active versions is operationally expensive and usually unnecessary.
- Version the entire API, not individual endpoints. Mixed versioning — where /v1/users and /v2/orders coexist — creates confusion. If an endpoint needs a breaking change, that change goes into the next full API version.
What Nobody Tells You About API Deprecation
Versioning is only half the problem. The other half is deprecation — actually removing old versions. This is where most teams fail.
The pattern I have seen work best is aggressive deprecation with generous timelines. Announce the deprecation date when the new version launches. Give clients 6-12 months. Send reminder emails at 90 days, 30 days, and 7 days. Then shut it down. Do not extend the deadline without a genuine business reason.
The temptation is to keep old versions running indefinitely because some client “might still need it.” This path leads to maintaining three, four, five API versions simultaneously, each with its own bugs, security patches, and operational overhead. It is unsustainable. Set timelines and enforce them.
Versioning Beyond REST
If you are using GraphQL, the versioning story is different. GraphQL is explicitly designed to evolve without versioning — you add new fields and deprecate old ones. This works well in practice, though it requires discipline around the deprecation process. The risk is that deprecated fields linger forever because removing them would break unknown clients.
For gRPC, Protocol Buffers have built-in field-level compatibility rules. You can add fields, deprecate fields, and maintain backward compatibility at the schema level. Version numbers in gRPC typically appear at the package level and follow similar patterns to URL path versioning in REST.
Key Takeaways
- Add API versioning from the first release. The cost is near zero and retrofitting it later is painful and risky. Start with /api/v1/ even if you think you will never need v2.
- URL path versioning is the best default for most teams. It is simple, explicit, cache-friendly, and easy for all consumers to understand.
- Header-based versioning suits APIs that need fine-grained control and are consumed primarily through client libraries, not ad-hoc HTTP requests.
- Define your breaking change policy in writing before you need it. This prevents arguments and sets clear expectations with API consumers.
- Deprecation is as important as versioning. Set aggressive timelines, communicate clearly, and enforce deadlines to avoid maintaining an ever-growing number of API versions.
