Isaac.

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

  1. Version early: Plan for changes
  2. Support multiple versions: Usually 2-3 versions
  3. Clear deprecation: Announce timelines
  4. Documentation: Clearly mark differences
  5. 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.