REST et API HTTP#
Les API REST (Representational State Transfer) dominent l’architecture des services web depuis les années 2000. Proposé par Roy Fielding dans sa thèse de doctorat en 2000, REST n’est pas un protocole mais un style architectural définissant un ensemble de contraintes qui, respectées, produisent des systèmes évolutifs, simples et interopérables.
Ce chapitre couvre les principes fondamentaux de REST, la conception d’URL, la sémantique des méthodes HTTP, les codes de statut, la pagination, le versioning, l’authentification, et la spécification OpenAPI. Il inclut un mini serveur REST avec la stdlib Python et des visualisations pédagogiques.
Les principes architecturaux de REST#
Les six contraintes de Fielding#
REST est défini par six contraintes architecturales. Respecter ces contraintes produit un système aux propriétés bien connues :
1. Client-Serveur : séparation stricte entre interface utilisateur (client) et stockage de données (serveur). Le client ne se préoccupe pas de la persistance des données ; le serveur ne se préoccupe pas de l’interface.
2. Sans état (Stateless) : chaque requête du client doit contenir toutes les informations nécessaires à sa compréhension. Le serveur ne conserve aucun contexte de session entre les requêtes. L’état de session est entièrement géré côté client (tokens, cookies).
3. Cache : les réponses doivent être qualifiées de cachables ou non-cachables. Un cache bien géré améliore les performances et réduit la charge serveur.
4. Interface uniforme : c’est la contrainte centrale de REST. Elle comprend quatre sous-contraintes : identification des ressources dans les requêtes (URI), manipulation des ressources par leurs représentations, messages auto-descriptifs, et hypermédia comme moteur d’état applicatif (HATEOAS).
5. Système en couches : le client ne sait pas s’il parle directement au serveur final ou à un intermédiaire (load balancer, cache, proxy). Cela permet la scalabilité et la sécurité.
6. Code à la demande (optionnel) : le serveur peut optionnellement envoyer du code exécutable au client (JavaScript, WebAssembly).
HATEOAS#
HATEOAS (Hypermedia As The Engine Of Application State) est la contrainte la plus souvent ignorée des API dites « REST ». Elle stipule que le client ne doit pas avoir de connaissance préalable des URIs de l’API : le serveur doit inclure dans chaque réponse les liens vers les actions disponibles.
{
"id": 42,
"nom": "Jean Dupont",
"statut": "actif",
"_links": {
"self": {"href": "/api/v1/utilisateurs/42"},
"modifier": {"href": "/api/v1/utilisateurs/42", "method": "PUT"},
"supprimer": {"href": "/api/v1/utilisateurs/42", "method": "DELETE"},
"commandes": {"href": "/api/v1/utilisateurs/42/commandes"},
"factures": {"href": "/api/v1/utilisateurs/42/factures"}
}
}
En pratique, HATEOAS est rarement implémenté complètement. La plupart des APIs modernes sont des APIs « REST-like » qui respectent les contraintes client-serveur, stateless, et interface uniforme, mais pas HATEOAS.
Conception d’URL et nommage des ressources#
Ressources et représentations#
En REST, tout est ressource : un utilisateur, une commande, un article, une image. Chaque ressource est identifiée par une URI (Uniform Resource Identifier). Une ressource peut avoir plusieurs représentations (JSON, XML, HTML) négociées via l’en-tête Accept.
Règles de nommage des URIs :
Utiliser des noms (pas des verbes) :
/utilisateurset non/obtenirUtilisateursUtiliser des pluriels pour les collections :
/utilisateurs,/commandesUtiliser des minuscules et des tirets pour les séparateurs de mots :
/articles-de-blogÉviter les extensions de fichiers :
/utilisateurs/42et non/utilisateurs/42.jsonLa hiérarchie reflète les relations :
/utilisateurs/42/commandes(commandes d’un utilisateur)
Exemples de conception d’URL :
Ressource |
URI |
Méthode |
Action |
|---|---|---|---|
Collection utilisateurs |
|
GET |
Lister |
Créer un utilisateur |
|
POST |
Créer |
Utilisateur spécifique |
|
GET |
Lire |
Modifier un utilisateur |
|
PUT |
Remplacer |
Mise à jour partielle |
|
PATCH |
Modifier |
Supprimer |
|
DELETE |
Supprimer |
Commandes d’un utilisateur |
|
GET |
Lister |
Paramètres de requête#
Les paramètres de requête servent à :
Filtrage :
GET /articles?categorie=tech&statut=publieTri :
GET /utilisateurs?tri=nom&ordre=ascPagination :
GET /commandes?page=3&limite=20Recherche :
GET /produits?q=ordinateur+portableSélection de champs :
GET /utilisateurs?champs=id,nom,email
URIs des actions non-CRUD
Certaines opérations ne correspondent pas aux actions CRUD standards. Deux approches :
Sous-ressource verbale :
POST /commandes/42/annuler— acceptable quand l’action modifie l’état de la ressourceUtiliser PATCH avec un champ statut :
PATCH /commandes/42avec{"statut": "annule"}— plus RESTful
Éviter absolument : GET /annulerCommande?id=42 — viole la sémantique des méthodes HTTP.
Idempotence et sécurité des méthodes HTTP#
Idempotence vs sécurité
Sûre (safe) : la méthode ne modifie pas l’état du serveur. GET, HEAD, OPTIONS sont sûres.
Idempotente : effectuer la même opération N fois produit le même résultat qu’une seule fois. DELETE /utilisateurs/42 effectué deux fois : la deuxième fois, la ressource est déjà absente (404), mais l’état du serveur est le même.
POST n’est ni sûre ni idempotente : deux POST identiques créent deux ressources distinctes.
Codes de statut HTTP appropriés#
Utiliser les codes de statut HTTP corrects est essentiel pour une API RESTful. Les clients (humains ou machines) s’appuient sur ces codes pour comprendre le résultat de chaque requête.
Pagination#
Renvoyer l’intégralité d’une collection peut être coûteux voire impossible pour des collections de millions d’éléments. La pagination est indispensable.
Pagination offset/limit#
GET /api/v1/articles?offset=40&limit=20
Retourne les articles 41 à 60. Simple à implémenter, mais présente des problèmes avec les données dynamiques (si un élément est inséré entre deux pages, des éléments peuvent être sautés ou dupliqués).
Réponse avec métadonnées :
{
"data": [...],
"pagination": {
"offset": 40,
"limit": 20,
"total": 1247,
"pages": 63
}
}
Pagination par curseur (cursor-based)#
Plus robuste pour les données dynamiques. Le curseur est un identifiant opaque pointant vers un élément spécifique :
GET /api/v1/articles?apres=eyJpZCI6NDF9&limite=20
Réponse :
{
"data": [...],
"pagination": {
"curseur_suivant": "eyJpZCI6NjF9",
"curseur_precedent": "eyJpZCI6MjF9",
"a_suivant": true,
"a_precedent": true
}
}
Link header (RFC 5988)#
Le standard HTTP définit l’en-tête Link pour la navigation paginée, utilisé par GitHub API :
Link: <https://api.example.com/articles?page=3>; rel="next",
<https://api.example.com/articles?page=1>; rel="first",
<https://api.example.com/articles?page=63>; rel="last"
Versioning d’API#
Les APIs évoluent. Le versioning permet d’introduire des changements incompatibles sans casser les clients existants.
Authentification et autorisation#
Basic Authentication#
L’authentification HTTP Basic encode identifiant:mot_de_passe en Base64 dans l’en-tête Authorization. Elle est simple mais doit toujours être utilisée sur HTTPS car le Base64 n’est pas du chiffrement.
Authorization: Basic dXNlcjpwYXNzd29yZA==
Bearer Token (JWT)#
Les JSON Web Tokens (JWT) sont le mécanisme d’authentification standard des APIs modernes. Un JWT est un token signé contenant des claims (revendications) :
Header.Payload.Signature
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxNTAwMDAwMH0.xxxx
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Le serveur vérifie la signature JWT sans accéder à une base de données, ce qui est stateless et scalable.
OpenAPI et Swagger#
OpenAPI (anciennement Swagger) est le standard de description des APIs HTTP. Une spécification OpenAPI (YAML ou JSON) décrit complètement l’API : routes, méthodes, paramètres, corps de requête, réponses, schémas de données, sécurité.
Extrait de spécification OpenAPI 3.0 :
============================================================
openapi: "3.0.3"
info:
title: API Utilisateurs
description: API REST pour la gestion des utilisateurs
version: "1.0.0"
contact:
name: Lôc Cosnier
email: contact@alkimya.fr
servers:
- url: https://api.example.com/v1
description: Production
- url: https://api-staging.example.com/v1
description: Staging
paths:
/utilisateurs:
get:
summary: Lister les utilisateurs
operationId: listerUtilisateurs
tags: [Utilisateurs]
security:
- bearerAuth: []
parameters:
- name: page
in: query
schema: {type: integer, default: 1}
- name: limite
in: query
schema: {type: integer, default: 20, maximum: 100}
- name: role
in: query
schema: {type: string, enum: [admin, user, moderateur]}
responses:
"200":
description: Liste paginée des utilisateurs
content:
application/json:
schema:
$ref: "#/components/schemas/PaginationUtilisateurs"
"401":
$ref: "#/components/responses/NonAuthenrifie"
post:
summary: Créer un utilisateur
operationId: creerUtilisateur
tags: [Utilisateurs]
requestBody:
required: true
content:
...
Spécification complète : 120 lignes
Mini serveur REST avec http.server#
Serveur REST démarré sur http://localhost:8888
--- Tests de l'API REST ---
GET /utilisateurs → 200
Total : 3 utilisateurs
GET /utilisateurs/1 → 200
Alice Martin (alice@example.com)
Liens HATEOAS : ['self', 'modifier', 'supprimer']
POST /utilisateurs → 201 (créé id=4)
POST /utilisateurs (doublon email) → 409: Email déjà utilisé
PATCH /utilisateurs/2 → 200: role=moderateur
---------------------------------------------------------------------------
JSONDecodeError Traceback (most recent call last)
Cell In[9], line 187
184 print(f"\nPATCH /utilisateurs/2 → {code}: role={data['role']}")
186 # DELETE
--> 187 code, _ = appel_api("DELETE", "/api/v1/utilisateurs/3"), ""
188 print(f"\nDELETE /utilisateurs/3 → {code[0]}")
190 # GET 404
Cell In[9], line 154, in appel_api(methode, chemin, corps)
152 try:
153 with urllib.request.urlopen(req) as resp:
--> 154 return resp.status, json.loads(resp.read().decode())
155 except urllib.error.HTTPError as e:
156 return e.code, json.loads(e.read().decode())
File /usr/lib/python3.13/json/__init__.py:346, in loads(s, cls, object_hook, parse_float, parse_int, parse_constant, object_pairs_hook, **kw)
341 s = s.decode(detect_encoding(s), 'surrogatepass')
343 if (cls is None and object_hook is None and
344 parse_int is None and parse_float is None and
345 parse_constant is None and object_pairs_hook is None and not kw):
--> 346 return _default_decoder.decode(s)
347 if cls is None:
348 cls = JSONDecoder
File /usr/lib/python3.13/json/decoder.py:345, in JSONDecoder.decode(self, s, _w)
340 def decode(self, s, _w=WHITESPACE.match):
341 """Return the Python representation of ``s`` (a ``str`` instance
342 containing a JSON document).
343
344 """
--> 345 obj, end = self.raw_decode(s, idx=_w(s, 0).end())
346 end = _w(s, end).end()
347 if end != len(s):
File /usr/lib/python3.13/json/decoder.py:363, in JSONDecoder.raw_decode(self, s, idx)
361 obj, end = self.scan_once(s, idx)
362 except StopIteration as err:
--> 363 raise JSONDecodeError("Expecting value", s, err.value) from None
364 return obj, end
JSONDecodeError: Expecting value: line 1 column 1 (char 0)
Visualisation des bonnes pratiques REST#
Résumé#
REST est avant tout un style architectural fondé sur des contraintes qui produisent des systèmes évolutifs et interopérables. Les APIs REST modernes sont construites autour de quelques principes simples : des URIs identifiant des ressources (noms, pas verbes), des méthodes HTTP avec leur sémantique correcte (idempotence, sécurité), des codes de statut appropriés, et une authentification par token.
La spécification OpenAPI permet de documenter, valider et générer du code client/serveur automatiquement. HATEOAS, bien que rarement implémenté en totalité, guide vers des APIs auto-descriptives où le client découvre les actions disponibles à partir des réponses du serveur.