API Versioning in the wild
You can read this for free here.
API Versioning in the wild
You can read this for free here.
You already know what APIs are, you know what versioning is, but why API versioning? Let us explore real-world strategies that allow frictionless deployments.

The challenge
Imagine this: you are working on a back-end system that exposes a public API to users. Your users might be external or other teams within your company. Now imagine an endpoint that creates orders. It takes the customer id, and a list of product IDs.
+--------+ POST /orders +------------------+
| Client | ---------------------------> | API Endpoint |
+--------+ | POST /orders |
+------------------+
Body
{
customer_id,
products[]
}
You have just been informed that you need to also add a mandatory store id the this.
+--------+ POST /orders +------------------+
| Client | --------------------------> | API Endpoint |
+--------+ | POST /orders |
+------------------+
Body
{
customer_id,
store_id,
products[]
}
Now, how do you deploy this change? If you deploy this, the clients calling this will break, and the only other option is to deploy together, probably over Google Meet. No one wants that. If only there was a way to deploy this without keeping the whole tech team on their toes for the whole day. And there is. That is what API versioning does.
Solution
As I have mentioned above, the solution is API versioning. You have probably seen this in the wild. Things like /api/v1/orders. What these do is ensure there is a smooth transition to a new version of the API without downtimes. This is how our above problem would look.
+--------+ POST /v1/orders +------------------+
| Client | -----------------------------> | API Endpoint |
+--------+ | POST /v1/orders |
+------------------+
Body
{
customer_id,
store_id,
products[]
}
Now, when the client team is ready to upgrade, they do so by calling this new endpoint. If not, the old one will keep on working for them.
Other versioning methods
While versions in the URL, like the one in the previous example, are common in many public APIs, it is not the only way to do it. There are several others, and like most things in system design, all these are trade-offs, and it is about picking the trade-offs you can live with. Start with the version in the URL one.
Version in URL
This is straightforward and easy for clients to use. It is also easy to evolve in the back-end. You just create a new endpoint with the new logic.
While it is easy to move fast, it can leave behind orphan endpoints if there is no system to make sure old endpoints are cleaned up. A good way to do this is to have a rule on how long an old endpoint would stay active; this could be the transition period between deployments.
Purists will also tell you that it violates REST principles and it is not clean, and as I said, it is all about the trade-offs you can live with.
Request headers
If you hate what having versions in the URL makes your URLs look like, you can choose to pass the version you are calling in the header.
GET /orders
API-Version: 1
Now this looks clean, but I want you to imagine how the logic in your codebase would look to support header versioning. In Rust, it would look like this.
pub fn post_orders(req: HttpRequest, svc: &OrderService) -> HttpResponse {
// 1) Version resolution (headers can be missing/garbled)
let version = match resolve_version(req.headers()) {
Ok(v) => v,
Err(e) => return e.into_http(),
};
// 2) Dispatch to a version-specific “adapter”
// (each adapter owns: decode + validate + mapping + response encoding)
match version {
ApiVersion::V1 => {
let v1_req = match decode_json::<v1::CreateOrderRequest>(&req.body) {
Ok(x) => x,
Err(e) => return bad_request(e),
};
if let Err(e) = v1::validate(&v1_req) {
return unprocessable(e);
}
let cmd = v1::to_command(v1_req);
match svc.create(cmd) {
Ok(order) => v1::encode_created(order),
Err(e) => e.into_http(),
}
}
ApiVersion::V2 => {
let v2_req = match decode_json::<v2::CreateOrderRequest>(&req.body) {
Ok(x) => x,
Err(e) => return bad_request(e),
};
if let Err(e) = v2::validate(&v2_req) {
return unprocessable(e);
}
let cmd = v2::to_command(v2_req);
match svc.create(cmd) {
Ok(order) => v2::encode_created(order),
Err(e) => e.into_http(),
}
}
}
}
You have one route exposed, but now the logic of version routing is all in this one route handler. This can get very complex, and that is why the Version in URL method is easier to manage. The ugliness you were running away from is now in your codebase, and you will hate working on this section. Another con is that this is very hard for CDNs to cache since they see endpoints, not version headers.
Query Parameter
This is when you add the version of the API to the query parameter.
POST /orders?v=1
This is mostly simple for clients, but it still calls the same route handler irrespective of the version. Like the header one, this will create very complex logic in your route handler. Microsoft does this in Azure, but Microsoft does a lot of questionable things, so I’m not surprised.
Content negotiation (Accept Header)
If this sounds familiar, you are a REST purist and you probably have blue hair. Anyway, as the title suggests, this is where you pass the version information in the Accept header. Your request would look something like this.
POST /orders
Accept: application/vnd.myapp.v2+json
If I were to ditch my version in URL one, this is the one I’d pick. In fact, this is what GitHub uses for their public-facing REST API. It combines content negotiation with Version in URL. Content negotiation can be a good way to beta-test API versions.
You already know where this is going. Yes, the logic in the back-end is complex, it has poor discoverability, and is generally hard to debug, but… and hear me out. You will have the bragging rights at those $2k per ticket tech conferences.
Evolving APIs
What happens to the old, discarded versions that no one uses? There is still remnant code that needs to be cleaned up, and without a proper strategy, things might get bad.
I am going to break this into two parts. The API server and the API client.
Server side
You may have noticed that I prefer versioning in the URL, and this is because I have been in the trenches, and this approach saved me. The flow I like looks like this.
New Requirement
|
v
Copy v1 Handler ------> Add Changes (v2)
| |
+------------------------+
|
v
Wait for Client Upgrades
|
v
Remove v1 Logic
This is easy to manage, but you can imagine how version control looks. That’s a trade-off you need to consider before picking this approach. If person A had some lines in v1, in v2 git blame will show those lines belonging to someone else. This can, however, be mitigated by strong PR discipline.
If you are using a single route, you can apply the same logic above, only that this time the deletion will happen in the route handler. This is cleaner, but I find it complex.
+------------------+
| v1 Handler |
| POST /orders |
+------------------+
|
v
+------------------------------+
| Add v2 Logic |
| (same route handler) |
| if version == v1 ... |
| if version == v2 ... |
+------------------------------+
|
v
+------------------------------+
| Clients Gradually Upgrade |
| v1 and v2 handled together |
+------------------------------+
|
v
+------------------------------+
| Remove v1 Conditions |
| Handler Simplified Again |
+------------------------------+
Client side
The major problem with the client side is remembering the route endpoints and the parameters they take. This is very error-prone and very hard to evolve. A solution would be using SDKs instead of raw endpoints.
You would use a tool like OpenAPIGenerator to generate client SDKs for the client language and consume them.
This buys you:
- End-to-end type safety
- Fewer errors
- Easy evolution
- Consistency due to a single source of truth
- Discoverability thanks to LSP
This shift will distribute most of the work across other teams. For example, the platform’s teams will handle the SDK generators. Managing SDKs involves significant release complexity, and you’ll likely need a build tool like Bazel to manage the repository.
Conclusion
API versioning isn’t just a fun thing to do; it can become messy without a clear process for evolving APIs. No matter your chosen approach, ensure you understand the trade-offs and build a culture around it.
메타데이터
- post_id
- 0ed4ef454f1d
- slug
- api-versioning-in-the-wild-0ed4ef454f1d
- url
- https://medium.com/@stanleymasinde/api-versioning-in-the-wild-0ed4ef454f1d
- canonical_url
- https://medium.com/@stanleymasinde/api-versioning-in-the-wild-0ed4ef454f1d
- author_url
- https://medium.com/@stanleymasinde
- status
- ok
- fetched_at
- 2026-06-09 14:34:10