Por qué versionar una API
Una API pública tiene clientes que no controlas: apps móviles publicadas, integraciones de partners, SDKs distribuidos. Un cambio incompatible (renombrar un campo, cambiar un tipo, quitar un endpoint) rompe a todos los clientes que no actualicen al mismo tiempo.
La solución: versionar. Cuando quieras evolucionar la API, publicas una nueva versión (v2) conviviendo con la anterior (v1). Los clientes migran a su ritmo y tú anuncias la deprecación de v1 con tiempo.
DRF ofrece 5 estrategias de versionado. La elección depende del ecosistema, los clientes y la cultura del equipo.
Las 5 estrategias de DRF
1. URLPathVersioning (la más común y recomendada)
La versión va en la URL:
/api/v1/productos/
/api/v2/productos/
- Ventaja: claridad total, fácil para debugging, cache amigable.
- Desventaja: duplicación de routing.
- La más usada: Stripe, GitHub, Twilio.
# settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2'],
}
# urls.py (proyecto)
urlpatterns = [
path('api/<str:version>/', include('api.urls')),
]
En la vista puedes acceder a request.version:
class ProductoViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.request.version == 'v2':
return ProductoV2Serializer
return ProductoV1Serializer
2. NamespaceVersioning
Similar a URLPath pero aprovecha los namespaces de Django:
# urls.py
urlpatterns = [
path('api/v1/', include('api.urls', namespace='v1')),
path('api/v2/', include('api.urls_v2', namespace='v2')),
]
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
}
- Ventaja: permite completamente distintos
urls.pypor versión, más flexible. - Desventaja: más archivos que mantener.
3. AcceptHeaderVersioning
La versión se envía en el header Accept:
Accept: application/json; version=2.0
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
}
- Ventaja: URLs limpias, semánticamente puro (RESTful content negotiation).
- Desventaja: difícil de probar desde navegador, cache más complicada.
- Usado por: GitHub API v3 (antes de v4 GraphQL).
4. HostNameVersioning
Subdominio por versión:
https://v1.api.miempresa.com/productos/
https://v2.api.miempresa.com/productos/
Requiere configuración DNS y de servidores. Poco práctico en la mayoría de casos.
5. QueryParameterVersioning
La versión como parámetro de query:
/api/productos/?version=v2
- Ventaja: muy simple de probar.
- Desventaja: ensucia la URL, fácil olvidar.
Convivencia de versiones con URLPathVersioning
El patrón recomendado: una sola view que actúa distinto según request.version:
# serializers.py
class ProductoV1Serializer(serializers.ModelSerializer):
class Meta:
model = Producto
fields = ['id', 'nombre', 'precio']
class ProductoV2Serializer(serializers.ModelSerializer):
precio_con_iva = serializers.SerializerMethodField()
class Meta:
model = Producto
fields = ['id', 'nombre', 'precio', 'precio_con_iva', 'stock', 'creado_en']
def get_precio_con_iva(self, obj):
return round(obj.precio * 1.21, 2)
# views.py
class ProductoViewSet(viewsets.ModelViewSet):
queryset = Producto.objects.all()
def get_serializer_class(self):
if self.request.version == 'v2':
return ProductoV2Serializer
return ProductoV1Serializer
Con un solo ViewSet sirves ambas versiones sin duplicar lógica de negocio.
Cuándo crear ViewSets separados
Si la diferencia entre v1 y v2 es estructural (distintos endpoints, distintos permisos), conviene separar:
# views.py
class ProductoV1ViewSet(viewsets.ModelViewSet):
serializer_class = ProductoV1Serializer
queryset = Producto.objects.all()
class ProductoV2ViewSet(viewsets.ModelViewSet):
serializer_class = ProductoV2Serializer
queryset = Producto.objects.annotate(total_vendidos=Sum('ventas__cantidad'))
Y en urls.py montas los routers separados:
router_v1 = DefaultRouter()
router_v1.register('productos', ProductoV1ViewSet, basename='producto-v1')
router_v2 = DefaultRouter()
router_v2.register('productos', ProductoV2ViewSet, basename='producto-v2')
urlpatterns = [
path('api/v1/', include(router_v1.urls)),
path('api/v2/', include(router_v2.urls)),
]
Validar la versión solicitada
ALLOWED_VERSIONS en settings rechaza versiones desconocidas con 404:
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2'],
'VERSION_PARAM': 'version',
}
Una petición a /api/v99/productos/ devuelve 404 Not Found.
Deprecación de versiones antiguas
Cuando quieras retirar una versión, añade headers estándar:
# middleware.py o en la view
class V1DeprecationMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if getattr(request, 'version', None) == 'v1':
response['Deprecation'] = 'true'
response['Sunset'] = 'Sun, 31 Dec 2026 23:59:59 GMT'
response['Link'] = '<https://api.miempresa.com/docs/migration-v2>; rel="deprecation"'
return response
Los headers Deprecation y Sunset son estándar (RFC 8594) y permiten a los clientes saber cuándo se retira la versión.
Documentar varias versiones con drf-spectacular
drf-spectacular detecta automáticamente el versionado de URL. Configuración:
# settings.py
SPECTACULAR_SETTINGS = {
'TITLE': 'Mi API',
'VERSION': '2.0.0',
'SERVE_INCLUDE_SCHEMA': False,
'SCHEMA_PATH_PREFIX': r'/api/v[0-9]+',
'SCHEMA_PATH_PREFIX_TRIM': True,
}
Por defecto, genera un único schema. Para un schema por versión, servir dos endpoints:
# urls.py
path('api/v1/schema/', SpectacularAPIView.as_view(api_version='v1'), name='schema-v1'),
path('api/v2/schema/', SpectacularAPIView.as_view(api_version='v2'), name='schema-v2'),
path('api/v1/docs/', SpectacularSwaggerView.as_view(url_name='schema-v1')),
path('api/v2/docs/', SpectacularSwaggerView.as_view(url_name='schema-v2')),
Estrategia de versionado en la práctica
Un patrón probado:
- Empieza con v1 desde el día 1:
DEFAULT_VERSION = 'v1'. Es barato y permite evolucionar después. - Cambios compatibles (añadir campo, nuevo endpoint, nuevo status code): NO crees v2. Añádelo a v1.
- Cambios incompatibles (quitar campo, cambiar tipo, cambiar comportamiento): crea v2. Mantén v1 en producción durante al menos 6-12 meses.
- Anuncia con tiempo: envía emails, añade banners en la UI, envía headers Deprecation.
- Monitoriza uso de versiones antiguas con
pg_stat_statements, logs o una métrica Prometheus por versión. - No cree v3 sin retirar primero v1: lo normal es mantener máximo 2 versiones simultáneas.
Errores comunes
- Ignorar ALLOWED_VERSIONS: una versión errónea en la URL da 500 en lugar de 404. Define siempre
ALLOWED_VERSIONS. - Copiar todo el código entre v1 y v2: duplica bugs. Mejor hacer que v1 invoque internamente a la lógica de v2 y transforme la respuesta.
- No migrar datos de clientes a v2: si v2 cambia el formato de datos almacenados, los clientes de v1 ven cosas raras. Mejor separar el storage del API contract.
- Confundir versionado de API con versionado de esquema BBDD: son cosas distintas. El schema de BBDD evoluciona con migraciones Django; la API con versioning DRF.
Resumen
| Estrategia | URL visible | Semántica REST | Fácil de probar | Recomendada para | |------------|-------------|----------------|-----------------|------------------| | URLPath | Sí | Baja | Muy fácil | La mayoría de APIs públicas | | Namespace | Sí | Media | Muy fácil | APIs con grandes diferencias entre versiones | | AcceptHeader | No | Alta | Difícil | APIs con filosofía REST estricta | | HostName | Sí (subdominio) | Media | Fácil | Grandes empresas con infraestructura DNS | | QueryParameter | Sí | Muy baja | Muy fácil | Prototipos, APIs internas |
Por defecto, elige URLPathVersioning: es clara, compatible con caches, debuggeable y es lo que los clientes esperan.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Django es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Django
Explora más contenido relacionado con Django y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Elegir la estrategia de versionado adecuada segun el caso (URL, header, namespace). Configurar DEFAULT_VERSIONING_CLASS en settings.py. Organizar urls.py para soportar /api/v1/ y /api/v2/ simultaneamente. Detectar la version desde la vista con request.version y serializers distintos por version. Deprecar endpoints antiguos con headers Deprecation y Sunset.
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje