API versioning becomes important when applications evolve without breaking existing consumers. Explore practical approaches for managing URL, query-string, and header-based versioning, along with backward compatibility and API lifecycle considerations. Learn how developers can introduce new API contracts while continuing to support existing clients. This discussion is useful for ASP.NET Core developers building maintainable APIs for real-world applications.
API versioning should be treated as **contract management**, not simply adding `/v2` to a URL.
An API is a contract between your service and its consumers. When the request or response contract changes in a breaking way, existing clients may stop working. Microsoft’s architecture guidance recommends planning for multiple versions when consumers cannot upgrade at the same time.
A common approach is URL-based versioning:
```text
GET /api/v1/customers/1001
GET /api/v2/customers/1001
```
Other strategies include:
```text
Header:
api-version: 2
Query string:
GET /api/customers/1001?api-version=2
```
The important decision is not which syntax looks better. The organisation should establish a **consistent versioning policy** across its APIs.
A practical strategy is:
### Non-breaking change
For example, adding an optional response property:
```json
{
"id": 1001,
"name": "Contoso",
"status": "Active"
}
```
may not require a new major API version if existing clients can safely ignore the additional property.
### Breaking change
Changing:
```json
{
"customerId": 1001
}
```
to:
```json
{
"customer": {
"id": 1001
}
}
```
can require a new contract because existing consumers may depend on the original structure.
For ASP.NET Core, current Microsoft documentation shows API versioning support for Minimal APIs through the `Asp.Versioning.Http` package, while ASP.NET Core's OpenAPI tooling can generate separate OpenAPI documents for different API versions.
For an enterprise API, I would define:
**Version identification → Compatibility rules → Deprecation policy → Documentation → Consumer migration → Testing → Retirement**
Avoid maintaining `v1`, `v2`, `v3`, and `v4` indefinitely without an ownership and retirement strategy.
A version should have a reason to exist, a migration path, and eventually a retirement plan.
The architectural question worth discussing is: **Should every breaking change create a new API version, or are there cases where the contract can evolve without versioning?**