.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) { /* ... */ }