Versionado de APIs REST con DRF

Avanzado
Django
Django
Actualizado: 19/04/2026

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.py por 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:

  1. Empieza con v1 desde el día 1: DEFAULT_VERSION = 'v1'. Es barato y permite evolucionar después.
  2. Cambios compatibles (añadir campo, nuevo endpoint, nuevo status code): NO crees v2. Añádelo a v1.
  3. Cambios incompatibles (quitar campo, cambiar tipo, cambiar comportamiento): crea v2. Mantén v1 en producción durante al menos 6-12 meses.
  4. Anuncia con tiempo: envía emails, añade banners en la UI, envía headers Deprecation.
  5. Monitoriza uso de versiones antiguas con pg_stat_statements, logs o una métrica Prometheus por versión.
  6. 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 - Autor del tutorial

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