Real APIs change over time without every consumer upgrading in lockstep, so versioning is worth planning for from early on rather than retrofitting later:
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class OrdersController : ControllerBase
{
[HttpGet("{id}"), MapToApiVersion("1.0")]
public IActionResult GetByIdV1(int id) => Ok(/* legacy shape */);
[HttpGet("{id}"), MapToApiVersion("2.0")]
public IActionResult GetByIdV2(int id) => Ok(/* current shape */);
}
URL-segment versioning (api/v1/orders) is the most discoverable approach for API consumers; header-based versioning is also common and keeps URLs stable across versions at the cost of being less obvious when browsing the API directly.
For documentation, OpenAPI/Swagger generation reads your controllers' attributes and types to produce an interactive spec automatically:
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(); // interactive docs, typically served at /swagger
}
Response type attributes make the generated documentation (and generated client SDKs) accurate about what each action can actually return, beyond what the compiler alone can infer from ActionResult<T>:
[HttpGet("{id}")]
[ProducesResponseType(typeof(Order), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Order>> GetById(int id) { /* ... */ }