On entend souvent “on a mis en place une API REST” dans les équipes. Mais quand on regarde les réponses JSON, il n’y a aucun lien. Juste des données brutes. Ce n’est pas du REST, c’est du CRUD exposé en HTTP.
La différence tient à un principe que la plupart des développeurs ignorent : HATEOAS.
Qu’est-ce que HATEOAS ?
HATEOAS signifie Hypermedia As The Engine Of Application State. C’est l’une des contraintes fondamentales du REST, définie par Roy Fielding dans sa thèse de 2000 (la même qui a inventé le terme REST).
Le principe est simple : un client REST ne doit pas avoir besoin de connaître les routes de l’API à l’avance. Il démarre depuis un point d’entrée, et découvre les actions disponibles en suivant les liens fournis dans chaque réponse.
Exactement comme on navigue sur le Web : on arrive sur une page, on lit les liens disponibles, on clique, on avance. On ne tape pas d’URL à la main à chaque étape.
CRUD over HTTP vs REST réel
Sans HATEOAS, une réponse typique ressemble à ça :
{
"id": 1,
"status": "pending",
"montant": 1500.00,
"client_id": 7
}
Le client qui reçoit ça doit déjà savoir :
- que pour valider, il faut faire
POST /contrats/1/valider/ - que pour annuler, c’est
DELETE /contrats/1/ - que le client est accessible via
GET /clients/7/
Cette connaissance est codée en dur côté client. Si l’API change une route, le client casse. Ce n’est pas de l’évolutivité, c’est du couplage fort déguisé.
Une réponse HATEOAS réelle
Avec HATEOAS, la même réponse devient auto-descriptive :
{
"id": 1,
"status": "pending",
"montant": 1500.00,
"client_id": 7,
"_links": {
"self": {
"href": "/api/contrats/1/",
"method": "GET"
},
"valider": {
"href": "/api/contrats/1/valider/",
"method": "POST"
},
"annuler": {
"href": "/api/contrats/1/",
"method": "DELETE"
},
"client": {
"href": "/api/clients/7/",
"method": "GET"
}
}
}
Le client n’a plus besoin de connaître les routes. Il lit les liens disponibles et sait quelles actions sont possibles dans l’état actuel de la ressource. Si le contrat est déjà validé, le lien valider n’apparaît pas dans la réponse. Le client n’a même pas à vérifier.
HATEOAS en pratique : les liens reflètent l’état de la ressource
C’est là que HATEOAS devient vraiment puissant. Les liens changent en fonction de l’état :
// Contrat en statut "pending"
"_links": {
"self": { "href": "/api/contrats/1/", "method": "GET" },
"valider": { "href": "/api/contrats/1/valider/", "method": "POST" },
"annuler": { "href": "/api/contrats/1/", "method": "DELETE" }
}
// Même contrat, statut "validated"
"_links": {
"self": { "href": "/api/contrats/1/", "method": "GET" },
"resilier": { "href": "/api/contrats/1/resilier/", "method": "POST" }
}
Le client ne code pas de logique conditionnelle (if status == "pending": show_validate_button). Il lit les liens disponibles et construit son interface en conséquence. L’API pilote l’état de l’application, c’est exactement ce que le nom HATEOAS signifie.
Implémentation avec Django REST Framework
DRF n’inclut pas HATEOAS nativement, mais on peut l’implémenter proprement avec un serializer dédié :
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]]:
request = self.context.get('request')
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
Chaque fois qu’un contrat est sérialisé, les liens reflètent son état courant. Aucune logique conditionnelle nécessaire côté client.
Modèle de maturité Richardson : où se situe votre API REST ?
Leonard Richardson a formalisé les niveaux de maturité d’une API REST :
| Niveau | Description | Exemple |
|---|---|---|
| 0 | Un seul endpoint, tout en POST | SOAP, XML-RPC |
| 1 | Ressources distinctes | GET /contrats/1 |
| 2 | Verbes HTTP corrects | POST /contrats/, DELETE /contrats/1 |
| 3 | HATEOAS | Réponses avec _links |
La plupart des APIs en production sont au niveau 2. Elles utilisent correctement les verbes HTTP et les ressources, mais s’arrêtent là. Le niveau 3 est ce que Fielding appelle “REST”.
Faut-il toujours implémenter HATEOAS ?
Honnêtement, non. HATEOAS ajoute de la complexité côté serveur et côté client (qui doit savoir lire et suivre les liens). Il est particulièrement adapté quand :
- L’API est publique et consommée par des clients tiers inconnus
- Le workflow est complexe et évolutif (plusieurs états, transitions conditionnelles)
- On veut réduire le couplage entre le client et l’API
Pour une API interne consommée par une seule application front que vous contrôlez, les niveaux 1 et 2 sont souvent suffisants et plus pragmatiques.
Mais comprendre HATEOAS change la façon de concevoir une API. Même sans l’implémenter complètement, inclure quelques liens clés dans vos réponses améliore déjà la découvrabilité et réduit la documentation nécessaire.
Tu travailles sur l’optimisation des requêtes Django ? Jette un œil à Django in_bulk() : pourquoi c’est mieux que filter() en masse.