REST API Versioning
Implement versioning strategies for evolving REST APIs.
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.
Related Articles
HTTP Status Codes
Learn how to understand and use HTTP status codes effectively in web applications and APIs.
Read More webAngular Component Communication
Implement patterns for component communication in Angular.
Read More webAPI Documentation with Swagger
Generate interactive API documentation with Swagger/OpenAPI.
Read More