Skip to main content
CodeOath
← All posts

.NET Core / Web API71 min total · 19 parts

Building REST APIs with ASP.NET Core: Routing, Middleware, and Dependency Injection

Part 16 of 19 · ~1 min

API Versioning and Documentation

Bench's availability check started simple — a plain boolean — and outgrew that shape once the kiosk UI wanted to show which minutes were actually free, not just whether the whole hour was booked solid:

[ApiController]
[Route("api/v{version:apiVersion}/tools/{toolId:int}/availability")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class AvailabilityController : ControllerBase
{
    [HttpGet, MapToApiVersion("1.0")]
    public IActionResult GetV1(int toolId) => Ok(new { isAvailable = true }); // legacy shape

    [HttpGet, MapToApiVersion("2.0")]
    public IActionResult GetV2(int toolId) => Ok(new[] { new { start = "14:00", end = "14:45" } }); // real shape
}

Putting the version straight in the URL (api/v1/...) is what Bench went with, mostly because it's impossible to miss — the kiosk firmware team could tell which version a device was still calling just by glancing at a log line. Sending the version in a header instead is the other well-worn approach, and it keeps every URL identical across versions; the trade is that nobody browsing the API casually can tell which version they're even looking at without checking a header nobody thinks to check.

For documentation, Swagger/OpenAPI reads Bench's own controller attributes and generates an interactive spec without any of it being hand-maintained:

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(); // interactive docs at /swagger
}

[ProducesResponseType] is what closes the gap between the generated docs and reality — ActionResult<T> alone only tells the compiler so much, and this attribute fills in the specific status codes and shapes a caller can actually expect:

[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(Reservation), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Reservation>> GetById(int toolId, Guid id) { /* ... */ }