El versionado de API es crítico para aplicaciones móviles donde los usuarios no siempre actualizan a la última versión. Mientras que el testing de contratos API asegura la compatibilidad entre frontend y backend, la estrategia de versionado maneja la complejidad de soportar múltiples versiones de cliente en producción. Esta guía cubre estrategias de versionado, compatibilidad retroactiva, actualizaciones forzadas y testing de múltiples versiones de API simultáneamente.
Estrategias de Versionado
Versionado por URL
val BASE_URL_V1 = "https://api.example.com/v1/"
val BASE_URL_V2 = "https://api.example.com/v2/"
interface ApiServiceV1 {
@GET("users/{id}")
suspend fun getUser(@Path("id") id: String): UserV1
}
Versionado por Header
class VersionInterceptor(private val apiVersion: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("API-Version", apiVersion)
.build()
return chain.proceed(request)
}
}
Testing Compatibilidad Retroactiva
Testear múltiples versiones de API requiere cobertura completa en todas las versiones de cliente soportadas. Como se discute en dominio del testing API, una suite de tests bien estructurada valida tanto requisitos funcionales como no funcionales.
class MultiVersionApiTest {
@Test
fun testUserEndpointV1() = runTest {
val apiV1 = createApiClient(version = "v1")
val user = apiV1.getUser("123")
assertNotNull(user.id)
assertNotNull(user.name)
}
@Test
fun testUserEndpointV2() = runTest {
val apiV2 = createApiClient(version = "v2")
val user = apiV2.getUser("123")
assertNotNull(user.fullName) // renombrado de 'name'
assertNotNull(user.phoneNumber) // nuevo campo
}
}
Estrategia de Actualización Forzada
data class VersionResponse(
val minimumVersion: String,
val latestVersion: String,
val forceUpdate: Boolean,
val message: String
)
class VersionChecker(private val versionService: VersionCheckService) {
suspend fun checkForUpdate(): UpdateStatus {
val response = versionService.checkVersion("android", BuildConfig.VERSION_NAME)
return when {
response.forceUpdate -> UpdateStatus.ForceUpdate(response.message)
isNewerVersion(BuildConfig.VERSION_NAME, response.latestVersion) ->
UpdateStatus.OptionalUpdate(response.latestVersion)
else -> UpdateStatus.UpToDate
}
}
}
@Test
fun testForceUpdateDetection() = runTest {
mockService.respondWith(VersionResponse(
minimumVersion = "2.0.0",
forceUpdate = true,
message = "Por favor actualiza para continuar"
))
val status = VersionChecker(mockService).checkForUpdate()
assertTrue(status is UpdateStatus.ForceUpdate)
}
Degradación Graceful
class FeatureManager(private val apiVersion: String) {
fun isFeatureAvailable(feature: Feature): Boolean {
return when (feature) {
Feature.USER_PREFERENCES -> apiVersion >= "2.0"
Feature.PUSH_NOTIFICATIONS -> apiVersion >= "1.5"
else -> true
}
}
}
@Test
fun testFeatureAvailability() {
val managerV1 = FeatureManager("1.0")
assertFalse(managerV1.isFeatureAvailable(Feature.USER_PREFERENCES))
val managerV2 = FeatureManager("2.0")
assertTrue(managerV2.isFeatureAvailable(Feature.USER_PREFERENCES))
}
A/B Testing Diferentes Versiones de API
El A/B testing de versiones de API en aplicaciones móviles permite rollout gradual y comparación de rendimiento antes del despliegue completo.
class ApiVersionSelector(private val userId: String, private val rolloutPercentage: Int) {
fun selectApiVersion(): String {
val userHash = userId.hashCode().absoluteValue
val bucket = userHash % 100
return if (bucket < rolloutPercentage) "v2" else "v1"
}
}
@Test
fun testGradualRollout() {
val versions = (1..1000).map {
ApiVersionSelector("user$it", 20).selectApiVersion()
}
val v2Count = versions.count { it == "v2" }
val percentage = (v2Count.toDouble() / versions.size) * 100
assertTrue(percentage in 15.0..25.0) // Aproximadamente 20%
}
Estrategia de Deprecación
En arquitecturas de microservicios, trackear métricas entre versiones de API se vuelve aún más crítico para identificar cuellos de botella de rendimiento y problemas de compatibilidad.
class DeprecationMonitor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
val deprecationWarning = response.header("Deprecation-Warning")
if (deprecationWarning != null) {
log.warn("API Deprecación: $deprecationWarning")
analytics.logEvent("api_deprecation_warning")
}
return response
}
}
Mejores Prácticas
- Versión en URL para cambios mayores - Claro, amigable con caché
- Usa headers para cambios menores - Flexible, compatible retroactivamente
- Siempre soporta versiones N-1 - Da tiempo a usuarios para actualizar
- Implementa mecanismo de actualización forzada - Correcciones críticas
- Usa feature flags - Rollout gradual, A/B testing
- Monitorea distribución de versiones - Saber cuándo deprecar
Conclusión
El versionado API para móviles requiere:
- Estrategia testing multi-versión
- Capas de compatibilidad retroactiva
- Mecanismos de actualización forzada
- Degradación graceful
- Infraestructura A/B testing
- Timelines de deprecación claros
El versionado apropiado asegura transiciones suaves mientras mantiene soporte para usuarios existentes, balanceando innovación con estabilidad.