Hey dev! En el artículo anterior te hablé toda la teoría que debes conocer para versionar un API, aquí iremos a la práctica, ¡arrancamos!
Vamos a ver cómo implementé el versionamiento en un proyecto real.
La estrategia que te enseñaré es el versionamiento por URL (URL Path Versioning)
El código está organizado siguiendo Clean Architecture:

Paso 1: Estructura de Controllers Versionados
La forma más limpia es crear controllers separados por versión:
// Controllers/ProductsV1Controller.cs
[ApiController]
[Route("api/v1/products")]
public class ProductsV1Controller : ControllerBase
{
private readonly IProductService _productService;
[HttpGet]
public async Task<IActionResult> GetAll()
{
var result = await _productService.GetAllAsync();
return Ok(result);
}
}
// Controllers/ProductsV2Controller.cs
[ApiController]
[Route("api/v2/products")]
public class ProductsV2Controller : ControllerBase
{
private readonly IProductService _productService;
[HttpGet]
public async Task<IActionResult> GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
var result = await _productService.GetAllPaginatedAsync(page, pageSize);
return Ok(result);
}
}
Nota: Ambos controllers consumen el mismo servicio. La diferencia está en el contrato de la API:
- v1 devuelve todos los productos
- v2 devuelve productos paginados
Paso 2: Configuración de Swagger
Para que Swagger muestre las versiones correctamente, configuramos múltiples documentos:
// Program.cs
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Products API",
Version = "v1"
});
options.SwaggerDoc("v2", new OpenApiInfo
{
Title = "Products API",
Version = "v2"
});
options.DocInclusionPredicate((docName, apiDesc) =>
{
var path = apiDesc.RelativePath ?? "";
return docName == "v1" && path.StartsWith("api/v1/")
|| docName == "v2" && path.StartsWith("api/v2/");
});
});
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Products API v1");
options.SwaggerEndpoint("/swagger/v2/swagger.json", "Products API v2");
options.RoutePrefix = "swagger";
});
Resultado: Un dropdown en Swagger que filtra endpoints por versión.
Paso 3: Servicios y más...
Tendrías una interfaz:
public interface IProductService
{
Task<ApiResponse<IEnumerable<ProductDto>>> GetAllAsync();
Task<PaginatedResponse<ProductDto>> GetAllPaginatedAsync(int page, int pageSize);
// otros...
}
Tu clase ProductService podría quedar así:
public class ProductService : IProductService
{
private readonly IProductRepository _repository;
private readonly IMapper _mapper;
private readonly ILogger<ProductService> _logger;
public ProductService(IProductRepository repository, IMapper mapper, ILogger<ProductService> logger)
{
_repository = repository;
_mapper = mapper;
_logger = logger;
}
public async Task<ApiResponse<IEnumerable<ProductDto>>> GetAllAsync()
{
_logger.LogDebug("Fetching all products");
var products = await _repository.GetAllAsync();
return ApiResponse<IEnumerable<ProductDto>>.SuccessResponse(_mapper.Map<IEnumerable<ProductDto>>(products));
}
public async Task<PaginatedResponse<ProductDto>> GetAllPaginatedAsync(int page, int pageSize)
{
_logger.LogDebug("Fetching paginated products - Page: {Page}, PageSize: {PageSize}", page, pageSize);
var (products, totalCount) = await _repository.GetAllPaginatedAsync(page, pageSize);
return new PaginatedResponse<ProductDto>
{
Items = _mapper.Map<IEnumerable<ProductDto>>(products),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
}
Tu puedes completar el repositorio para traer los datos o cambiar la clase de servicio, lo importante aquí es que sepas cómo versionar el API y eso lo hacemos en los pasos 1 y 2.
Como resultado tendrás un API que tiene dos versiones, y en swagger lo mirarías así:

Fíjate que ese dropdown funciona y el tag al costado del nombre del API ahora cambia según la versión seleccionada de v1 a v2.
Y eso es todo por ahora, nos vemos en el próximo artículo, cracks!
Si esta entrada te ha gustado, te invito a compartirla!
Créditos de imagen de portada: Foto de Hans-Jurgen Mager en Unsplash
