Pourquoi la sémantique HTTP compte
Une API REST « propre » ne se contente pas de renvoyer des données : elle communique un état via le code HTTP, des en-têtes et (parfois) un corps de réponse. Une sémantique prévisible permet au client de prendre des décisions simples : réessayer ou non, afficher un message, invalider un cache, corriger une requête, etc. L’objectif est d’être cohérent : mêmes scénarios → mêmes codes, mêmes formats d’erreur, mêmes en-têtes.
Succès : choisir entre 200, 201 et 204
200 OK : succès avec représentation
Utilisez 200 lorsque la réponse contient une représentation utile : lecture d’une ressource, liste, ou résultat d’une opération qui renvoie un corps.
- GET d’une ressource existante →
200+ JSON de la ressource. - PATCH/PUT réussi et vous renvoyez la ressource mise à jour →
200+ JSON. - POST qui exécute une action et renvoie un résultat (sans créer une nouvelle ressource) →
200+ JSON du résultat.
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "v7"
{ "id": "u_123", "email": "a@exemple.com", "status": "active" }201 Created : création d’une nouvelle ressource
Utilisez 201 quand une requête crée effectivement une nouvelle ressource. La bonne pratique est d’inclure l’en-tête Location pointant vers l’URL canonique de la ressource créée. Vous pouvez aussi renvoyer un corps (souvent la représentation de la ressource créée) pour éviter un GET supplémentaire.
Quand renvoyer 201 avec Location (étapes pratiques)
- 1) Le serveur génère l’identifiant (ou accepte un identifiant client si votre design le permet).
- 2) Il persiste la ressource.
- 3) Il construit l’URL canonique (stable) de la ressource.
- 4) Il renvoie
201+Location+ (optionnel) corps JSON.
HTTP/1.1 201 Created
Location: /users/u_123
Content-Type: application/json
{ "id": "u_123", "email": "a@exemple.com", "status": "pending" }À éviter : renvoyer 200 pour une création si vous pouvez renvoyer 201. Le code 201 simplifie la logique client (création confirmée + emplacement connu).
- Écoutez le fichier audio avec l'écran éteint.
- Obtenez un certificat à la fin du programme.
- Plus de 5000 cours à découvrir !
Téléchargez l'application
204 No Content : succès sans corps
Utilisez 204 lorsque l’opération réussit mais qu’il n’y a rien d’utile à renvoyer. Un 204 ne doit pas contenir de corps. C’est typique pour une suppression, ou une mise à jour où le client n’a pas besoin de représentation.
Quand utiliser 204 (étapes pratiques)
- 1) Exécuter l’opération (ex. suppression).
- 2) Ne pas sérialiser de JSON.
- 3) Renvoyer
204(éventuellement avec des en-têtes utiles, ex. invalidation cache côté client).
HTTP/1.1 204 No Content
Cache-Control: no-storeCas fréquent : DELETE /users/u_123 → 204 si la suppression a eu lieu. Si la ressource n’existe pas, préférez 404 (voir plus bas) pour rester explicite.
Erreurs côté client : 400, 401, 403, 404, 409, 422
Les codes 4xx indiquent que le client doit modifier sa requête (ou son contexte d’authentification/autorisation). Pour garder une sémantique prévisible, distinguez : format invalide (400), authentification (401), autorisation (403), absence (404), conflit (409), validation métier/champs (422).
400 Bad Request : requête mal formée
Utilisez 400 quand la requête est syntaxiquement incorrecte ou impossible à interpréter : JSON invalide, type inattendu, paramètre manquant au niveau protocolaire, pagination incohérente, etc.
HTTP/1.1 400 Bad Request
Content-Type: application/json
{ "error": { "code": "bad_request", "message": "Corps JSON invalide." } }401 Unauthorized : authentification requise ou invalide
Utilisez 401 si l’utilisateur n’est pas authentifié ou si le jeton/credential est invalide/expiré. En HTTP, 401 implique souvent un en-tête WWW-Authenticate (selon le schéma utilisé).
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Content-Type: application/json
{ "error": { "code": "unauthorized", "message": "Authentification requise." } }403 Forbidden : authentifié mais non autorisé
Utilisez 403 quand l’utilisateur est authentifié mais n’a pas les droits nécessaires. Évitez de détailler la règle exacte côté réponse si cela révèle des informations sensibles (rôles, existence de ressources, politiques internes).
HTTP/1.1 403 Forbidden
Content-Type: application/json
{ "error": { "code": "forbidden", "message": "Accès refusé." } }404 Not Found : ressource inexistante (ou non révélée)
Utilisez 404 quand la ressource demandée n’existe pas. Dans certains contextes, vous pouvez aussi renvoyer 404 au lieu de 403 pour éviter de révéler l’existence d’une ressource (stratégie de non-divulgation). L’important est d’être cohérent sur une même famille d’endpoints.
Structurer une 404 utile (sans fuite d’informations)
- Inclure un code d’erreur stable (machine-readable).
- Inclure un message générique (humain) sans confirmer des détails sensibles.
- Optionnel : inclure l’identifiant demandé si ce n’est pas sensible (souvent acceptable car fourni par le client).
- Inclure un identifiant de corrélation (request id) via en-tête ou champ pour support.
HTTP/1.1 404 Not Found
Content-Type: application/json
X-Request-Id: 8f3c2a
{ "error": { "code": "not_found", "message": "Ressource introuvable.", "details": { "resource": "user", "id": "u_999" } } }409 Conflict : conflit d’état ou d’unicité
Utilisez 409 quand la requête est valide mais ne peut pas être appliquée à cause d’un conflit avec l’état actuel : contrainte d’unicité (email déjà utilisé), version concurrente, transition d’état impossible (ex. tenter d’activer un compte déjà supprimé), ou conflit d’ETag (optimistic locking).
Structurer une 409 utile
- Indiquer la nature du conflit via un code stable (ex.
conflict,duplicate,etag_mismatch). - Donner un message actionnable (ex. « choisir un autre email », « recharger puis réessayer »).
- Ne pas divulguer d’informations sur des ressources appartenant à d’autres utilisateurs (ex. « cet email appartient à X » est à proscrire).
HTTP/1.1 409 Conflict
Content-Type: application/json
{ "error": { "code": "duplicate", "message": "Valeur déjà utilisée.", "details": { "field": "email" } } }422 Unprocessable Entity : validation métier/champs
Utilisez 422 lorsque la requête est bien formée (syntaxe OK) mais échoue la validation : champs hors bornes, format d’email valide mais non accepté, règle métier (date de fin avant date de début), etc. Cela aide le client à distinguer une erreur de parsing (400) d’une erreur de validation (422).
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{ "error": { "code": "validation_failed", "message": "Certains champs sont invalides.", "details": { "fields": [ { "name": "password", "reason": "too_short" } ] } } }Incidents serveur : 500 et 503
500 Internal Server Error : erreur inattendue
Utilisez 500 pour une erreur non prévue côté serveur. Le corps doit rester générique : ne renvoyez pas de stack trace, de requêtes SQL, ni de détails d’infrastructure. En revanche, fournissez un identifiant de corrélation pour permettre au support de retrouver les logs.
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
X-Request-Id: 8f3c2a
{ "error": { "code": "internal_error", "message": "Une erreur est survenue." } }503 Service Unavailable : indisponibilité temporaire
Utilisez 503 quand le service est temporairement indisponible (maintenance, surcharge, dépendance critique indisponible). Si vous savez quand le client peut réessayer, utilisez Retry-After.
HTTP/1.1 503 Service Unavailable
Retry-After: 30
Content-Type: application/json
{ "error": { "code": "service_unavailable", "message": "Service temporairement indisponible, réessayez plus tard." } }Format d’erreur cohérent : messages actionnables sans divulgation
Principes de base
- Stabilité : un champ
error.codestable (machine-readable) est plus fiable qu’un texte. - Lisibilité : un
messagecourt, orienté action. - Détails contrôlés : un objet
detailsoptionnel pour les champs invalides, sans données sensibles. - Corrélation : un
X-Request-Id(ou équivalent) pour l’investigation. - Pas de fuite : ne révélez pas l’existence de comptes, d’emails, de ressources d’autres utilisateurs, ni la cause interne exacte (ex. « violation index unique users_email_idx »).
Gabarit recommandé
{
"error": {
"code": "...",
"message": "...",
"details": { }
}
}Stratégie côté client (ce que l’API doit permettre)
Pour aider le client à afficher des messages appropriés sans dépendre de textes, alignez vos réponses sur une stratégie simple :
- 4xx : le client peut corriger (afficher un message, surligner un champ, demander une reconnexion).
- 409 : le client doit résoudre un conflit (changer une valeur, recharger l’état, gérer concurrence).
- 5xx : le client peut proposer de réessayer plus tard (avec backoff), et afficher un message générique.
En-têtes utiles : ETag et Cache-Control (quand pertinent)
ETag : cache et concurrence optimiste
ETag est un identifiant de version d’une représentation. Il sert à (1) éviter des transferts inutiles et (2) protéger des mises à jour concurrentes.
Étapes pratiques : GET conditionnel avec ETag
- 1) Le serveur renvoie
ETagsurGET. - 2) Le client renvoie
If-None-Matchsur unGETultérieur. - 3) Si inchangé, le serveur renvoie
304 Not Modifiedsans corps.
GET /users/u_123
If-None-Match: "v7"
HTTP/1.1 304 Not Modified
ETag: "v7"Étapes pratiques : mise à jour protégée
- 1) Le client lit la ressource et récupère
ETag. - 2) Le client envoie
If-Matchlors duPUT/PATCH. - 3) Si l’ETag ne correspond plus, renvoyer
409(ou412 Precondition Failedsi vous adoptez cette variante) avec un message « recharger puis réessayer ».
PATCH /users/u_123
If-Match: "v7"
Content-Type: application/json
{ "status": "active" }
HTTP/1.1 200 OK
ETag: "v8"
Content-Type: application/json
{ "id": "u_123", "status": "active" }Cache-Control : contrôler la mise en cache
Cache-Control indique si et comment une réponse peut être mise en cache. Utilisez-le lorsque c’est pertinent (ressources publiques, données rarement modifiées, ou au contraire données sensibles).
- Données sensibles / personnalisées :
Cache-Control: no-store(évite stockage). - Données cacheables :
Cache-Control: public, max-age=60(ouprivatesi spécifique à un utilisateur). - Validation : combinez avec
ETagpour des caches efficaces.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, max-age=60
ETag: "v7"
{ "id": "u_123", "status": "active" }Checklist de sémantique prévisible (à appliquer endpoint par endpoint)
| Scénario | Code | Corps | En-têtes conseillés |
|---|---|---|---|
| Lecture OK | 200 | Oui | ETag, Cache-Control (si pertinent) |
| Création OK | 201 | Optionnel | Location, (ETag si vous renvoyez la représentation) |
| Suppression OK | 204 | Non | Cache-Control (selon sensibilité) |
| JSON invalide / paramètres incohérents | 400 | Oui (erreur) | X-Request-Id |
| Non authentifié | 401 | Oui (erreur) | WWW-Authenticate |
| Non autorisé | 403 | Oui (erreur) | X-Request-Id |
| Ressource absente (ou masquée) | 404 | Oui (erreur) | X-Request-Id |
| Conflit (unicité / concurrence) | 409 | Oui (erreur) | ETag (si lié à version), X-Request-Id |
| Validation métier/champs | 422 | Oui (erreur) | X-Request-Id |
| Erreur interne | 500 | Oui (générique) | X-Request-Id |
| Indisponibilité temporaire | 503 | Oui (générique) | Retry-After, X-Request-Id |