Réseaux & Web · L2 · Section 5/7
REST et APIs
Progression
#REST et APIs
Concevoir une API, c'est définir des ressources stables, des sémantiques claires et des erreurs prévisibles. Cette page déroule le cycle de vie d'une ressource requête par requête, puis les décisions structurantes: idempotence, pagination, versionnement, sécurité.
Objectifs d'apprentissage
- Définir des ressources cohérentes et leurs méthodes, statuts et en-têtes associés.
- Rendre les créations rejouables (clé d'idempotence) et les mises à jour sûres (ETag/If-Match).
- Documenter erreurs, pagination, filtrage, tri; choisir un versionnement.
- Connaître auth Bearer, rate-limit et la surface CORS d'une API.
Prérequis: page HTTP (méthodes, statuts, ETag).
#Principes REST
Les ressources exposent des « choses » (users, posts, cours) via des URLs stables (/users/42), jamais des verbes: /users/42 suffit, /getUser?id=42 n'apporte rien et se lit mal dans les logs. Chaque requête porte tout son contexte (authentification, préférences): c'est le stateless, qui autorise l'équilibrage de charge sans affinité de session et le cache naïf des GET.
Les représentations se négocient par Accept/Content-Type, avec application/json en pratique. Le serveur contrôle la forme des réponses; le client exprime une préférence. Ce contrat façonne l'URL et le schéma, pas l'inverse.
#Trace: cycle de vie complet d'une ressource
Une création suivie d'une lecture, vus du réseau:
1> POST /api/users HTTP/1.12> Host: api.example.com3> Authorization: Bearer eyJhbGci…4> Content-Type: application/json5> Idempotency-Key: 7d1c-4f0a-9b2e6>7> {"name": "Ada"}8 9< HTTP/1.1 201 Created10< Location: /api/users/4211< Content-Type: application/json12<13< {"id": 42, "name": "Ada", "createdAt": "2026-09-01T10:00:00Z"}1> GET /api/users/42 HTTP/1.12> If-None-Match: "42-v1"3>4< HTTP/1.1 200 OK5< ETag: "42-v1"6<7< {"id": 42, "name": "Ada", "createdAt": "2026-09-01T10:00:00Z"}Chaque ligne a une fonction: Idempotency-Key protégera contre le double envoi (voir plus bas), Location dit où trouver la ressource créée sans que le client devine l'URL, ETag arme la validation du prochain GET et la concurrence optimiste.
#Animation: cycle d'une requête REST
#Idempotence en pratique
GET, PUT, DELETE sont idempotents par construction. Le problème est POST: un clic double, un proxy qui rejoue, une perte de réseau qui masque la réponse, et la ressource est créée deux fois. Deux mécanismes le règlent, à des étages différents:
- Clé d'idempotence: le client envoie
Idempotency-Key: <uuid>à chaque tentative d'une même intention. Le serveur stocke la première réponse (statut + corps) pour cette clé et la renvoie telle quelle aux tentatives suivantes au lieu de retraiter. - ETag/If-Match pour les mises à jour: le serveur refuse (
412 Precondition Failed) une modification fondée sur une version périmée. C'est la concurrence optimiste décrite en page HTTP.
Trace d'un rejeu géré:
1> POST /api/users HTTP/1.12> Idempotency-Key: 7d1c-4f0a-9b2e <- même clé que la première tentative3>4< HTTP/1.1 201 Created <- réponse rejouée, pas de doublon en base5< Location: /api/users/42#Pagination, filtrage, tri
- Offset (
?limit=20&offset=40): simple, mais une insertion entre deux pages décale les lignes (doublons/omissions) et l'offset profond coûte au serveur. - Curseur (
?cursor=eyJpZCI6NDJ9&limit=20): la page suivante part d'une clé stabilisée (id, date de création). Stable sous insertion, constant en coût; exige un tri déterministe.
Le filtrage et le tri sont explicites et validés: ?status=active&sort=-createdAt. Refuser les filtres inconnus (400) plutôt que les ignorer silencieusement évite au client de croire filtrer quand il reçoit tout. Pour les collections, une réponse type {"items": […], "nextCursor": "…"} sépare données de la mécanique de pagination et laisse place aux métadonnées.
#Diagramme: erreur de validation (422, Problem Details)
#Statuts: le minimum vital
| Scénario | Statut | En-tête/clé |
|---|---|---|
| Création réussie | 201 | Location |
| Action sans corps | 204 | aucun |
| Conflit métier (doublon, état incompatible) | 409 | corps Problem Details |
| Entité invalide au sens métier | 422 | détails par champ |
| Quota dépassé | 429 | Retry-After |
| Précondition échouée (If-Match périmé) | 412 | ETag attendu |
#Sécurité et limites
- Authentification: jetons Bearer (JWT/OAuth2) dans
Authorization. Portée minimale, expiration courte, rotation des refresh tokens. - Rate-limit: rendu visible via
RateLimit-Limit,RateLimit-Remaining,Retry-After, et 429 structurés. Invisible, il se manifeste comme des bugs aléatoires côté client. - CORS: origines explicites; jamais
Access-Control-Allow-Origin: *combiné àAccess-Control-Allow-Credentials: true(refusé par les navigateurs, et contournable serait catastrophique). Voir la page CORS. - Validation: refuser tôt (400/422) avec messages clairs, sans divulguer d'éléments internes (stack traces, requêtes SQL).
#Versionnement
- Chemin (
/v1/): visible, cacheable, simple à router; à la charge du client de migrer. - En-tête (
Accept: application/vnd.example.v1+json): URLs stables, mais moins visible et plus difficile à déboguer avec curl. - Sous-domaine (
v1.api.example.com): sépare les déploiements, complexifie DNS et CORS.
Stratégie saine: conserver les versions stables tant qu'elles servent, déprécier avec préavis et en-têtes Deprecation/Sunset, éviter les ruptures silencieuses (champ renommé sans version).
#OpenAPI minimal (documentation vivante)
Une spec succincte consolide le contrat et alimente la génération de clients:
1openapi: 3.0.32info:3 title: Mini API4 version: 1.0.05paths:6 /users:7 post:8 summary: Créer un utilisateur9 parameters:10 - in: header11 name: Idempotency-Key12 schema: { type: string, format: uuid }13 requestBody:14 required: true#Alternatives
- GraphQL: schéma typé, requêtes flexibles côté client; attention au caching HTTP (souvent POST, difficile à mettre en cache) et au coût des requêtes arbitrairement lourdes.
- gRPC: contrats protobuf sur HTTP/2, efficace pour les services internes; moins adapté aux clients navigateur publics.
#Animation: décisions de design
#Exercice : activer/désactiver un utilisateur
Concevez les endpoints et statuts pour cette fonctionnalité, puis comparez les deux options.
#Instructions
- Option A:
POST /users/42/disableetPOST /users/42/enable(style RPC). - Option B:
PATCH /users/42avec{"active": false}(style REST). - Pour chaque option, listez: méthode, corps, statut(s) de succès, idempotence, comportement si déjà désactivé.
- Rédigez la réponse d'erreur si l'utilisateur n'existe pas, et si l'appelant n'a pas le droit.
#Correction
Option A: POST n'est pas idempotent par défaut; ici l'opération l'est de fait (désactiver un utilisateur déjà désactivé ne change rien: 200 ou 204, pas 409). Avantage: l'URL dit l'intention, plusieurs variantes d'action peuvent coexister; inconvénient: prolifération d'URLs ad hoc, hors modèle ressources.
Option B: PATCH modifie l'état; idempotent (renvoyer {"active": false} deux fois laisse le même état). Avantage: cohérence avec le reste de la collection, un seul mécanisme pour tous les champs; inconvénient: l'intention (« désactiver ») est moins explicite, les transitions métier (notification, audit) doivent être déclenchées par la valeur du champ.
Dans les deux cas: 404 si l'utilisateur n'existe pas; 403 si l'appelant est authentifié mais non autorisé (voir page HTTP: 401 = « qui êtes-vous », 403 = « non »). Le choix A vs B est un arbitrage d'équipe: le critère décisif est le nombre d'actions produit (disable, reset-password, impersonate…) au-delà duquel les PATCH sur des champs ne suffisent plus.