API Versioning the Simple Way: URL + Header Patterns that Scale
7 min read
Intermediate
Why API Versioning Saves Time Later
Your API works perfectly today, but requirements change. A mobile app needs new fields, a partner integration relies on the old response format, and internal teams want different data shapes. Without versioning, you're stuck choosing between breaking existing clients or creating messy workarounds that slow down every future change.
Versioning lets you evolve your API without forcing everyone to upgrade at once. Clients pick the version they understand, and you can introduce breaking changes in new versions while keeping old ones stable. This approach keeps your API flexible and your clients happy.
You'll build versioned Minimal API endpoints using both URL paths and headers. By the end, you'll know which strategy fits your use case and how to implement versioning that actually scales in production.
URL Path Versioning
URL versioning puts the version number directly in the endpoint path, like /v1/products or /v2/products. This approach is visible, straightforward, and works perfectly in browsers, API testing tools, and documentation. When a client makes a request, the version is explicit in the URL itself.
The Asp.Versioning.Http library provides first-class support for versioning in Minimal APIs. You configure versioning in your service collection, then map endpoints to specific versions. The framework handles routing requests to the correct endpoint based on the URL pattern.
Here's a basic setup that creates two versions of a products endpoint, each returning different response shapes.
The version set defines which versions your API supports. Each endpoint uses MapToApiVersion to declare which version it belongs to. When a client requests /v1/products, they get the simpler response. When they request /v2/products, they get the enhanced version with currency and stock information.
Header-Based Versioning
Header versioning keeps your URLs clean by moving the version information into HTTP headers. Clients send an api-version header with their requests, and the framework routes to the appropriate endpoint. This pattern works well for internal APIs where you control the clients and prefer cleaner URLs.
The configuration is similar to URL versioning, but you specify the version location in the options. The framework reads the header value and matches it to the correct endpoint version.
Here's how to set up header-based versioning with a fallback to a default version.
Program.cs
using Asp.Versioning;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new HeaderApiVersionReader("api-version");
});
var app = builder.Build();
app.MapGet("/products", (int id) =>
{
return Results.Ok(new { Id = id, Name = "Widget V1" });
})
.HasApiVersion(1, 0);
app.MapGet("/products", (int id) =>
{
return Results.Ok(new { Id = id, Name = "Widget V2", Tags = new[] { "new" } });
})
.HasApiVersion(2, 0);
app.Run();
Clients send requests to /products with an api-version: 2.0 header to get version 2. Omitting the header uses the default version. The ReportApiVersions option adds headers to responses showing which versions are available.
Supporting Both URL and Header Versioning
Some APIs need to support multiple versioning strategies at once. Public APIs might use URL versioning for external clients while internal tools use headers. The Asp.Versioning library lets you combine readers so clients can specify versions either way.
The UrlSegmentApiVersionReader handles URL-based versions, while HeaderApiVersionReader processes header-based versions. You combine them using ApiVersionReader.Combine, and the framework accepts whichever format the client provides.
This approach gives you maximum flexibility without duplicating endpoint definitions.
Now clients can use either /v2/orders/123 or send /orders/123 with an api-version: 2.0 header. Both requests route to the same v2 endpoint. This flexibility helps during migrations when different clients adopt versioning at different paces.
Deprecating Old Versions Gracefully
Eventually you'll need to retire old API versions. The versioning library supports deprecation markers that inform clients when they're using outdated versions. This gives teams time to migrate without sudden breaking changes.
When you mark a version as deprecated, response headers include deprecation warnings. You can combine this with logging to track which clients still use old versions and coordinate migration timelines.
Here's how to mark version 1.0 as deprecated while keeping it functional.
Program.cs
using Asp.Versioning;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(2, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
});
var app = builder.Build();
var versionSet = app.NewApiVersionSet()
.HasDeprecatedApiVersion(1, 0)
.HasApiVersion(2, 0)
.Build();
app.MapGet("/v{version:apiVersion}/customers/{id}", (int id, ILogger logger) =>
{
logger.LogWarning("Client using deprecated API v1.0 for customer {Id}", id);
return Results.Ok(new { Id = id, Name = "Legacy Customer" });
})
.WithApiVersionSet(versionSet)
.MapToApiVersion(1, 0);
app.MapGet("/v{version:apiVersion}/customers/{id}", (int id) =>
{
return Results.Ok(new
{
Id = id,
Name = "Customer",
Email = "customer@example.com",
CreatedAt = DateTime.UtcNow
});
})
.WithApiVersionSet(versionSet)
.MapToApiVersion(2, 0);
app.Run();
Clients using version 1.0 receive responses with deprecation headers, and the server logs their usage. You can monitor these logs to identify who needs to migrate, then set a sunset date when the old version stops working entirely.
Mistakes to Avoid
Versioning too granularly: Don't create a new version for every small change. Minor tweaks like adding optional fields can stay in the current version. Only increment versions for breaking changes that would fail existing clients if deployed without warning.
Duplicating all business logic: Teams sometimes copy entire service layers for each version. Instead, share as much internal code as possible and only version the DTOs and endpoint handlers. Use mapping logic to transform shared domain models into version-specific responses.
No sunset policy: Running too many versions creates maintenance overhead. Establish a clear deprecation timeline (like six months notice) before removing old versions. Communicate the timeline through response headers, documentation, and direct outreach to major clients.
Inconsistent version format: Pick one versioning scheme and stick with it. Mixing URL versioning and header versioning across different endpoints confuses clients. Use combined readers only when you need both strategies for the same endpoints.
Try It Yourself
Build a versioned API that returns different product information across two versions. You'll set up URL-based versioning and test both versions with simple HTTP requests.
Steps
dotnet new web -n VersioningDemo
cd VersioningDemo
dotnet add package Asp.Versioning.Http
Replace Program.cs with the code below
dotnet run
Test with: curl http://localhost:5000/v1/products/42
Test v2: curl http://localhost:5000/v2/products/42
Version 1 returns a simple JSON object with id, name, and price. Version 2 includes additional fields like currency, stock count, and category. The response headers show which API versions are available.
Real-World Integration Notes
When building versioned APIs in production, you'll often need to share data access code while keeping API contracts separate. Create version-specific response DTOs that map from shared domain models. This keeps database logic in one place while letting each API version return exactly what its clients expect.
For logging and monitoring, tag requests with the API version. This helps you track which versions see the most traffic and identify when it's safe to deprecate old versions. Most observability tools can group metrics and traces by custom tags.
If you're using OpenAPI (Swagger), configure separate documents for each version. The Asp.Versioning library integrates with Swashbuckle to generate version-specific documentation automatically. Clients can explore each version's schema independently.
Consider version negotiation carefully. Some teams default to the newest version when clients don't specify one. Others default to the oldest for maximum compatibility. Choose the strategy that matches your client base and document it clearly in your API guidelines.
Quick FAQ
Should I version my API in the URL or in headers?
URL versioning is simpler for clients and easier to debug in browsers. Use it when your API is public or consumed by varied clients. Header versioning keeps URLs cleaner and works better for internal APIs with tight control over consumers. Most teams start with URL versioning.
Can I support multiple versions at once?
Yes, the Asp.Versioning.Http library lets you run multiple API versions simultaneously. You map each endpoint to a version number, and the framework routes requests accordingly. Plan to deprecate old versions within a defined timeline to avoid maintenance sprawl.
How do I handle breaking changes across versions?
Keep backward-compatible changes in the same version. For breaking changes like removing fields or changing response shapes, create a new version. Share as much internal logic as possible between versions while keeping distinct DTOs. Deprecate old versions with clear timelines and migration guides.