API Versioning Strategies (URL, Header, Query String)
Mind Map Summary
- API Versioning
- Why?: To allow an API to evolve over time without breaking existing client applications.
- Key Principle: A change is considered “breaking” if it requires a client to be updated to continue functioning correctly (e.g., removing a field, changing a data type).
- Common Versioning Strategies
- 1. URL Path Versioning
- Example:
GET /api/v1/products
vs.GET /api/v2/products
- Pros: Explicit and highly visible. Easy for developers to browse and test in a browser.
- Cons: Technically violates the REST principle that a URI should represent a single, unique resource.
- Example:
- 2. Query String Versioning
- Example:
GET /api/products?api-version=1.0
- Pros: Simple to implement and keeps the core URI clean.
- Cons: Can be messy and easy for clients to omit, leading them to get an unexpected version.
- Example:
- 3. Header Versioning
- Example:
GET /api/products
with headerX-Api-Version: 1.0
- Pros: Keeps the URI clean. Considered by many to be the purest RESTful approach as it separates the resource (URI) from its representation (version).
- Cons: Less visible to developers; cannot be tested directly in a browser URL.
- Example:
- 1. URL Path Versioning
Core Concepts
Why Version an API?
As an API matures, you will inevitably need to make changes. Some changes are non-breaking (like adding a new, optional field to a response), but many are breaking (like renaming a field, removing a property, or changing the structure of the response). If you make a breaking change without versioning, all client applications currently using your API will break. Versioning provides a contract: you are telling your clients that they can rely on a specific version of the API (v1
) to remain stable, while you are free to make improvements and breaking changes in a new version (v2
).
1. URL Path Versioning
This is the most common and straightforward approach. The version is embedded directly in the URL path. It’s immediately obvious to anyone looking at the URL which version of the API they are interacting with.
- Pros: Highly explicit, easy to explore and test, simple to route requests to different controller code.
- Cons: It couples the resource identifier (the URI) to a specific point-in-time representation of that resource, which some argue is not purely RESTful.
2. Query String Versioning
This approach specifies the version as a parameter in the query string. It’s easy to implement and easy to default to the latest version if the parameter is omitted.
- Pros: Simple, clean URLs.
- Cons: Less explicit than URL path versioning. It can be easy for developers to forget to include the query parameter.
3. Header Versioning
This approach uses a custom HTTP request header (e.g., X-Api-Version
) to specify the version. This is often favored by REST purists because the URI (/api/products
) remains a canonical identifier for the resource, and the header simply requests a different representation of that resource.
- Pros: Cleanest URLs. Separates the resource from its representation.
- Cons: Not visible to the end-user in the browser. Requires specific tooling (like Postman or curl) to set the header, making casual exploration more difficult.
Practice Exercise
Using the Asp.Versioning.Mvc
library, create a single controller with two action methods that respond to the same route. One method should be for API version 1.0, and the other for 2.0. Configure the application to support URL-segment-based versioning (e.g., /api/v1/products
).
Answer
First, you need to add the required NuGet package: dotnet add package Asp.Versioning.Mvc.ApiExplorer
Code Example
1. Program.cs
- Configure API Versioning
using Asp.Versioning;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// 1. Add API Versioning Services
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true; // Adds api-supported-versions header
options.ApiVersionReader = new UrlSegmentApiVersionReader(); // Read version from URL path
});
var app = builder.Build();
app.MapControllers();
app.Run();
2. The Versioned Controller
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
[ApiController]
// The {v:apiVersion} template parameter is automatically populated by the library
[Route("api/v{v:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
// This action handles GET requests for version 1.0
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetV1()
{
var products = new[]
{
new { Id = 1, Name = "Laptop" }, // V1 has simple objects
new { Id = 2, Name = "Mouse" }
};
return Ok(products);
}
// This action handles GET requests for version 2.0
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetV2()
{
var products = new[]
{
// V2 has more detailed objects with a new 'Description' field
new { Id = 1, Name = "Laptop", Description = "A powerful laptop" },
new { Id = 2, Name = "Mouse", Description = "A wireless mouse" }
};
return Ok(products);
}
}
Explanation
- Configuration: In
Program.cs
, we configure the API versioning service. We set a default version, report supported versions in response headers, and most importantly, we set theApiVersionReader
toUrlSegmentApiVersionReader
. This tells the library to look for the version in the URL path (e.g.,/v1/
). - Controller Routing: The
Route
attribute on the controller now includesv{v:apiVersion}
. The versioning library uses this template to extract the version from the URL. [ApiVersion]
Attribute: We decorate the controller with[ApiVersion("1.0")]
and[ApiVersion("2.0")]
to declare that this controller knows how to handle requests for both versions.[MapToApiVersion]
Attribute: This is the key to mapping a specific request to a specific method. We apply[MapToApiVersion("1.0")]
to theGetV1
method and[MapToApiVersion("2.0")]
to theGetV2
method.
How it Works in Practice:
- A
GET
request to/api/v1/products
will be routed to theGetV1()
method. - A
GET
request to/api/v2/products
will be routed to theGetV2()
method, which returns a different data structure. - A
GET
request to/api/products
(without a version) will be routed to the default version,v1
, because we setAssumeDefaultVersionWhenUnspecified = true
.