Isaac.

web

REST API Versioning

Implement versioning strategies for evolving REST APIs.

By Emem IsaacJuly 26, 20243 min read
#rest#api#versioning#compatibility#backward compatibility
Share:

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.

Share:

Written by Emem Isaac

Expert Software Engineer with 15+ years of experience building scalable enterprise applications. Specialized in ASP.NET Core, Azure, Docker, and modern web development. Passionate about sharing knowledge and helping developers grow.

Ready to Build Something Amazing?

Let's discuss your project and explore how my expertise can help you achieve your goals. Free consultation available.

💼 Trusted by 50+ companies worldwide | ⚡ Average response time: 24 hours