REST API Versioning
Implement versioning strategies for evolving REST APIs.
By EMEPublished: February 20, 2025
restapiversioningcompatibilitybackward compatibility
A Simple Analogy
API versioning is like updating a building blueprint. You need a plan to transition from old to new without disrupting tenants.
Why Versioning?
- Compatibility: Support multiple clients
- Evolution: Change APIs safely
- Migration: Gradual client updates
- Stability: Keep old versions available
- Communication: Clear upgrade path
URL Path Versioning
[ApiController]
[Route("api/v{version}/[controller]")]
public class OrdersController : ControllerBase
{
// GET api/v1/orders
[HttpGet]
public async Task<ActionResult<List<OrderV1>>> GetOrdersV1()
{
var orders = await _context.Orders.ToListAsync();
return orders.Select(o => new OrderV1
{
Id = o.Id,
CustomerId = o.CustomerId,
Total = o.Total
}).ToList();
}
}
[ApiController]
[Route("api/v{version}/[controller]")]
public class OrdersController : ControllerBase
{
// GET api/v2/orders
[HttpGet]
public async Task<ActionResult<List<OrderV2>>> GetOrdersV2()
{
var orders = await _context.Orders.Include(o => o.Items).ToListAsync();
return orders.Select(o => new OrderV2
{
Id = o.Id,
CustomerId = o.CustomerId,
Total = o.Total,
Items = o.Items.Select(i => new OrderItemDto
{
ProductId = i.ProductId,
Quantity = i.Quantity,
Price = i.Price
}).ToList()
}).ToList();
}
}
Header-Based Versioning
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetOrders(
[FromHeader(Name = "api-version")] string apiVersion)
{
return apiVersion switch
{
"1.0" => Ok(await GetOrdersV1()),
"2.0" => Ok(await GetOrdersV2()),
_ => BadRequest("Unsupported API version")
};
}
private async Task<List<OrderV1>> GetOrdersV1() { /* ... */ }
private async Task<List<OrderV2>> GetOrdersV2() { /* ... */ }
}
// Client request
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/api/orders");
request.Headers.Add("api-version", "2.0");
Content Negotiation
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
[HttpGet("{id}")]
[Produces("application/vnd.example.order-v1+json")]
[Produces("application/vnd.example.order-v2+json")]
public async Task<IActionResult> GetOrder(int id, string accept)
{
var order = await _context.Orders.FindAsync(id);
if (order == null) return NotFound();
if (accept.Contains("order-v1"))
{
return Ok(new OrderV1 { /* ... */ });
}
return Ok(new OrderV2 { /* ... */ });
}
}
// Client request
var request = new HttpRequestMessage(HttpMethod.Get, "api/orders/1");
request.Headers.Accept.ParseAdd("application/vnd.example.order-v2+json");
Deprecation Headers
[HttpGet("old-endpoint")]
public IActionResult OldEndpoint()
{
Response.Headers.Add("Deprecation", "true");
Response.Headers.Add("Sunset", new DateTimeOffset(2025, 12, 31, 23, 59, 59, TimeSpan.Zero).ToString("r"));
Response.Headers.Add("Link", "</api/v2/orders>; rel=\"successor-version\"");
return Ok();
}
Backward Compatibility
public class VersionCompatibilityMiddleware
{
private readonly RequestDelegate _next;
public async Task InvokeAsync(HttpContext context)
{
var apiVersion = context.Request.Headers["api-version"].ToString() ?? "1.0";
context.Items["api-version"] = apiVersion;
await _next(context);
}
}
public class OrderDto
{
public int Id { get; set; }
public string CustomerId { get; set; }
public decimal Total { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<OrderItemDto>? Items { get; set; } // V2 only
}
Migration Strategy
// V1: Old behavior
[HttpGet("v1/products")]
public async Task<List<ProductV1>> GetProductsV1()
{
return await _context.Products.Select(p => new ProductV1
{
Id = p.Id,
Name = p.Name,
Price = p.Price
}).ToListAsync();
}
// V2: Enhanced with inventory
[HttpGet("v2/products")]
public async Task<List<ProductV2>> GetProductsV2()
{
return await _context.Products
.Include(p => p.Inventory)
.Select(p => new ProductV2
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
StockLevel = p.Inventory.AvailableQuantity
}).ToListAsync();
}
Best Practices
- Version early: Plan for changes
- Support multiple versions: Usually 2-3 versions
- Clear deprecation: Announce timelines
- Documentation: Clearly mark differences
- Testing: Test across versions
Related Concepts
- API gateways
- Contract testing
- Semantic versioning
- Breaking changes
Summary
API versioning enables safe evolution. Use path versioning for clarity, header versioning for flexibility, or content negotiation for elegance. Always provide clear migration paths.