En los equipos se escucha con frecuencia “tenemos una API REST”. Pero al revisar las respuestas JSON, no hay ningún enlace. Solo datos en crudo. Eso no es REST, es CRUD expuesto en HTTP.

La diferencia la marca un principio que la mayoría de los desarrolladores ignora: HATEOAS.

¿Qué es HATEOAS en una API REST?

HATEOAS significa Hypermedia As The Engine Of Application State. Es una de las restricciones fundamentales del REST, definida por Roy Fielding en su tesis del año 2000.

El principio es claro: un cliente REST no debería necesitar conocer de antemano las rutas de la API. Descubre las acciones disponibles siguiendo los enlaces que proporciona cada respuesta. Igual que navegamos en la web.

CRUD sobre HTTP frente al REST real

Sin HATEOAS, el cliente tiene que conocer de antemano las rutas. Ese conocimiento está codificado en duro. Si la API cambia una ruta, el cliente se rompe. No es evolución, es acoplamiento fuerte disfrazado.

Una respuesta HATEOAS real

Con HATEOAS, la respuesta se vuelve autodescriptiva gracias a _links. El cliente lee los enlaces disponibles y sabe qué acciones son posibles en el estado actual. Si el contrato ya está validado, el enlace valider no aparece.

HATEOAS en la práctica: los enlaces reflejan el estado del recurso

Los enlaces cambian según el estado del recurso. El cliente no necesita codificar lógica condicional. La API conduce el estado de la aplicación.

Implementación con Django REST Framework y HATEOAS

DRF no incluye HATEOAS de forma nativa. Se implementa con un serializer:

from rest_framework import serializers
from .models import Contrat, ContratStatus


class ContratSerializer(serializers.ModelSerializer):
    _links = serializers.SerializerMethodField()

    class Meta:
        model = Contrat
        fields = ['id', 'status', 'montant', 'client_id', '_links']

    def get__links(self, obj: Contrat) -> dict[str, dict[str, str]]:
        base = f'/api/contrats/{obj.pk}/'
        links: dict[str, dict[str, str]] = {
            'self': {'href': base, 'method': 'GET'},
        }
        if obj.status == ContratStatus.PENDING:
            links['valider'] = {'href': f'{base}valider/', 'method': 'POST'}
            links['annuler'] = {'href': base, 'method': 'DELETE'}
        if obj.status == ContratStatus.VALIDATED:
            links['resilier'] = {'href': f'{base}resilier/', 'method': 'POST'}
        if obj.client_id:
            links['client'] = {'href': f'/api/clients/{obj.client_id}/', 'method': 'GET'}
        return links

Modelo de madurez Richardson: ¿dónde está tu API REST?

NivelDescripciónEjemplo
0Un único endpoint POSTSOAP, XML-RPC
1Recursos distintosGET /contrats/1
2Verbos HTTP correctosPOST /contrats/
3HATEOASRespuestas con _links

¿Hay que implementar HATEOAS siempre?

No. Tiene sentido cuando la API es pública, el flujo de trabajo es complejo o se quiere reducir el acoplamiento. Para una API interna, los niveles 1-2 suelen ser suficientes.


¿Trabajas en optimización de consultas Django? Echa un vistazo a Django in_bulk(): por qué es mejor que filter() en masa.