Every API that survives long enough will need versioning. The question is not whether to version, but how to do it in a way that minimizes pain for both your team and your consumers. After maintaining public APIs for over six years across multiple products, I want to share what has worked and what has not.
URL Segment Versioning — Simple and Honest
URL segment versioning (/api/v1/orders) is the most common approach, and for good reason. It is explicit, easy to route, and impossible to miss. In .NET, the Asp.Versioning.Http package makes this straightforward.
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
The downside is URL pollution — every route now carries version baggage, and clients need to update URLs when migrating. But in practice, this explicitness is a feature, not a bug.
Header Versioning — Clean URLs, Hidden Complexity
Header-based versioning (Api-Version: 2) keeps URLs clean but hides the version from casual inspection. Every debugging session, every curl command, every log entry becomes slightly harder because the version is not visible in the URL.
options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
// Client must remember to set the header
httpClient.DefaultRequestHeaders.Add("X-Api-Version", "2");
I have seen this work well for internal APIs where all consumers are controlled by the same organization. For public APIs, I strongly prefer URL versioning.
Additive Changes Avoid Versioning Entirely
The best version change is no version change. Many "breaking changes" can be restructured as additive changes. Adding a new field to a response is not breaking. Adding a new optional query parameter is not breaking. Adding a new endpoint is not breaking.
// V1 response
public class OrderResponse
{
public Guid Id { get; init; }
public string Status { get; init; }
public decimal Total { get; init; }
}
// Still V1 — adding fields is non-breaking
public class OrderResponse
{
public Guid Id { get; init; }
public string Status { get; init; }
public decimal Total { get; init; }
public string? CurrencyCode { get; init; } // New, nullable, non-breaking
public DateTimeOffset? EstimatedDelivery { get; init; } // New, nullable, non-breaking
}
Sunset Policies Save You
Document and enforce sunset policies from day one. When you release v2, announce that v1 will be supported for exactly 12 months. Set Sunset and Deprecation headers on old versions. Monitor usage and reach out to consumers who have not migrated.
app.MapGet("/api/v1/orders", (HttpContext context) =>
{
context.Response.Headers.Append("Sunset", "Sat, 01 Mar 2026 00:00:00 GMT");
context.Response.Headers.Append("Deprecation", "true");
context.Response.Headers.Append("Link",
"</api/v2/orders>; rel=\"successor-version\"");
// ... handle request
});
Without sunset policies, you end up maintaining every version indefinitely. I have worked on systems with five active API versions and the maintenance burden is brutal. Set boundaries early, communicate them clearly, and enforce them on schedule.
Comments (2)
I tried this approach and it works perfectly!
Thanks for your comment — glad it helped!
Well written and easy to follow. Keep it up!
Leave a Comment