Objectifs : erreurs prévisibles, réponses cohérentes, traçabilité exploitable
Une API « propre » ne se contente pas de renvoyer un statut HTTP : elle fournit un format d’erreur uniforme, permet de relier une réponse à des logs via un identifiant de corrélation (traceId), et évite toute fuite d’informations sensibles. L’objectif est double : aider le client à corriger sa requête (erreurs côté client) et aider l’équipe back-end à diagnostiquer rapidement (erreurs côté serveur) sans exposer de détails internes.
Mise en place d’un format d’erreur uniforme
Structure recommandée
Adoptez un objet JSON stable, identique pour toutes les erreurs, quel que soit le statut HTTP. Exemple de structure :
{ "errorCode": "USER_EMAIL_INVALID", "message": "L’adresse e-mail est invalide.", "details": [ { "field": "email", "issue": "format" } ], "traceId": "01HZY2..."}errorCode: code stable, lisible, non localisé. Sert au support, au monitoring, et à la logique côté client.message: message destiné à l’utilisateur final ou au développeur client (selon votre produit). Peut être localisé.details: informations structurées pour guider la correction (liste d’erreurs de champs, contraintes, paramètres). Optionnel.traceId: identifiant de corrélation, présent dans la réponse et dans les logs. Toujours présent si possible.
Champs optionnels utiles (sans surcharger)
Selon vos besoins, vous pouvez ajouter des champs optionnels, en restant cohérent :
timestamp: date ISO 8601 côté serveur.path: chemin de la requête (ex./users/123).method: verbe HTTP.hint: suggestion courte (ex. « Vérifiez le format RFC 5322 ») sans révéler d’implémentation interne.retryable: booléen indiquant si un retry a du sens (utile pour 503/429).
Localisation (i18n) des messages
Deux approches courantes :
- Localiser
messagecôté serveur en fonction deAccept-Language. Avantage : expérience utilisateur immédiate. Inconvénient : nécessite un catalogue de traductions côté API. - Laisser
messagegénérique et localiser côté client viaerrorCode. Avantage : API plus simple. Inconvénient : le client doit maintenir les traductions.
Bonne pratique : conserver errorCode comme source de vérité et ne jamais le traduire. Si vous localisez message, assurez-vous qu’il reste stable et non ambigu.
- É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
Étapes pratiques : standardiser les erreurs
- Définir un contrat : documentez la structure JSON et la liste des
errorCode(au moins par domaine fonctionnel). - Créer un “ErrorResponse builder” : une fonction unique qui construit l’objet (ajout de
traceId,timestamp, etc.). - Centraliser le traitement des exceptions : un middleware/filtre global qui intercepte les erreurs et renvoie le format uniforme.
- Imposer des tests : tests d’intégration vérifiant que chaque catégorie d’erreur renvoie bien
errorCodeettraceId.
Catégorisation des erreurs : une taxonomie opérationnelle
Catégoriser permet d’avoir des réponses cohérentes, des logs exploitables et des alertes pertinentes. Voici une classification pratique, alignée sur les causes réelles.
1) Erreurs côté client (entrées invalides)
Cas typiques : paramètres manquants, formats incorrects, incohérences simples. Réponse : errorCode explicite + details structurés.
{ "errorCode": "REQUEST_PARAMETER_INVALID", "message": "Un ou plusieurs paramètres sont invalides.", "details": [ { "field": "limit", "issue": "must_be_between", "min": 1, "max": 100 } ], "traceId": "01HZY2..."}2) Authentification / autorisation
Distinguez clairement :
- Non authentifié : absence/invalidité des identifiants (token expiré, signature invalide).
- Non autorisé : authentifié mais droits insuffisants.
Évitez de divulguer des détails (ex. « utilisateur inexistant ») qui facilitent l’énumération. Préférez des messages neutres.
{ "errorCode": "AUTH_REQUIRED", "message": "Authentification requise.", "traceId": "01HZY2..."}{ "errorCode": "FORBIDDEN", "message": "Accès non autorisé.", "traceId": "01HZY2..."}3) Conflits
Conflits d’état : ressource déjà existante, concurrence, version obsolète, règle d’unicité. Le details peut indiquer la nature du conflit sans exposer de données sensibles.
{ "errorCode": "RESOURCE_CONFLICT", "message": "Conflit : la ressource ne peut pas être modifiée dans son état actuel.", "details": [ { "reason": "version_mismatch" } ], "traceId": "01HZY2..."}4) Ressources absentes
Quand une ressource n’existe pas (ou n’est pas visible), gardez une réponse cohérente. Selon votre modèle de sécurité, vous pouvez choisir de ne pas distinguer « absent » et « non autorisé » pour éviter la fuite d’information. Dans tous les cas, le format reste identique.
{ "errorCode": "RESOURCE_NOT_FOUND", "message": "Ressource introuvable.", "traceId": "01HZY2..."}5) Erreurs serveur
Exceptions inattendues, bugs, erreurs d’infrastructure. Ne renvoyez jamais la stack trace au client. Utilisez traceId pour permettre le diagnostic côté serveur.
{ "errorCode": "INTERNAL_ERROR", "message": "Une erreur interne est survenue.", "traceId": "01HZY2..."}Journalisation (logs) : diagnostiquer sans fuite de secrets
Principes de base
- Logs structurés (JSON) : facilitent la recherche et les agrégations (par
traceId,errorCode,status). - Pas de secrets : ne loggez jamais tokens, mots de passe, clés API, cookies de session, numéros complets de cartes, etc.
- Masquage systématique : appliquez une stratégie de redaction (ex. remplacer par
***ou hachage partiel). - Éviter les dumps : ne loggez pas le corps complet des requêtes/réponses en production sans filtrage.
Correlation ID / traceId : génération et propagation
Le traceId doit suivre la requête de bout en bout (API → services internes → dépendances). Deux options :
- Accepter un identifiant entrant (ex. en-tête
X-Request-Id) et le réutiliser s’il est valide. - Générer un identifiant si absent, puis le renvoyer dans la réponse et l’injecter dans tous les logs.
Étapes pratiques :
- Middleware d’entrée : lire
X-Request-Id(ou équivalent), sinon générer un UUID/ULID. - Contexte de requête : stocker le
traceIddans un contexte accessible (thread-local, context async, etc.). - Propagation sortante : ajouter
traceIdaux appels HTTP sortants (en-tête) et aux messages asynchrones. - Réponse : inclure
traceIddans le JSON d’erreur et idéalement dans un en-tête (ex.X-Request-Id).
Exemple de log structuré (serveur)
{ "level": "ERROR", "event": "api_request_failed", "traceId": "01HZY2...", "errorCode": "INTERNAL_ERROR", "http": { "method": "POST", "path": "/orders", "status": 500 }, "user": { "id": "u_123" }, "durationMs": 84}Remarque : la stack trace peut être loggée côté serveur (niveau ERROR) mais jamais renvoyée au client. Si vous la loggez, assurez-vous qu’elle ne contient pas de secrets (certaines bibliothèques incluent des valeurs de paramètres).
Mapping interne des exceptions vers des réponses HTTP cohérentes
Une API maintenable évite les try/catch dispersés. Le mapping doit être centralisé : chaque exception métier/technique est convertie en un couple (statut HTTP, errorCode, message, details).
Étapes pratiques : construire un mapping
- Définir une hiérarchie d’erreurs internes : par exemple
DomainError,AuthError,ConflictError,NotFoundError,DependencyError. - Associer un
errorCodeà chaque type : stable, versionné si nécessaire. - Créer un handler global : transforme toute exception en
ErrorResponseuniforme. - Traiter les exceptions inconnues : fallback vers
INTERNAL_ERROR+ log complet côté serveur.
Table de mapping (exemple)
| Catégorie interne | Exemple d’exception | Statut HTTP | errorCode | Notes |
|---|---|---|---|---|
| Entrées invalides | InvalidParameterError | 400 | REQUEST_PARAMETER_INVALID | Inclure details par champ |
| Authentification | UnauthenticatedError | 401 | AUTH_REQUIRED | Message neutre |
| Autorisation | ForbiddenError | 403 | FORBIDDEN | Ne pas révéler la règle exacte |
| Absent | NotFoundError | 404 | RESOURCE_NOT_FOUND | Option : masquer si non autorisé |
| Conflit | ConflictError | 409 | RESOURCE_CONFLICT | Peut inclure reason |
| Dépendance indisponible | UpstreamTimeoutError | 503 | SERVICE_UNAVAILABLE | Marquer retryable=true |
| Inattendu | Exception | 500 | INTERNAL_ERROR | Log complet + traceId |
Exemple de pseudo-code de handler global
function handleError(err, req): traceId = req.context.traceId if err is DomainError: return json(err.httpStatus, { errorCode: err.code, message: localize(err.messageKey, req.locale), details: err.details, traceId: traceId }) if err is UpstreamTimeoutError: return json(503, { errorCode: "SERVICE_UNAVAILABLE", message: "Service temporairement indisponible.", details: [{ "dependency": err.dependency }], traceId: traceId, retryable: true }) log.error({ traceId, err }) return json(500, { errorCode: "INTERNAL_ERROR", message: "Une erreur interne est survenue.", traceId: traceId })Stratégies de résilience : timeouts, dépendances, 503 cohérents
Timeouts : éviter les requêtes qui pendent
Sans timeouts, une dépendance lente peut saturer vos threads/connexions et provoquer un effet domino. Appliquez des timeouts à chaque appel sortant (HTTP, base de données, cache) et différenciez :
- Timeout de connexion : temps max pour établir la connexion.
- Timeout de lecture : temps max d’attente de réponse.
- Timeout global : budget total par requête (deadline).
Étapes pratiques :
- Définir un budget par endpoint (ex. 800 ms) et le propager aux dépendances.
- Configurer les clients (HTTP/DB) avec des timeouts explicites.
- Gérer l’erreur : transformer un timeout en réponse cohérente (souvent 503) avec
retryable.
Gestion des dépendances : dégradation contrôlée
Quand une dépendance est instable, vous pouvez :
- Limiter la concurrence (bulkhead) : éviter qu’une dépendance consomme toutes les ressources.
- Circuit breaker : couper temporairement les appels après un taux d’échec élevé.
- Fallback : réponse partielle, cache, ou fonctionnalité dégradée (si acceptable).
Dans tous les cas, la réponse d’erreur doit rester uniforme et traçable.
Réponses 503 et JSON d’erreur cohérent
Pour une indisponibilité temporaire (dépendance down, surcharge, maintenance), renvoyez une erreur cohérente, sans détails internes (pas de nom de cluster, d’IP, etc.). Exemple :
{ "errorCode": "SERVICE_UNAVAILABLE", "message": "Service temporairement indisponible. Veuillez réessayer plus tard.", "details": [ { "type": "dependency", "name": "payment" } ], "traceId": "01HZY2...", "retryable": true}Bonnes pratiques :
- Inclure
traceIdpour relier l’incident aux logs. - Rester vague dans
message(pas d’informations d’infrastructure). - Optionnel : ajouter un en-tête
Retry-Aftersi vous connaissez un délai raisonnable.
Checklist de mise en œuvre
- Un seul format JSON d’erreur (mêmes champs, mêmes types) pour toutes les routes.
errorCodestable et non localisé ;messageéventuellement localisé.traceIdgénéré/propagé et présent dans la réponse + logs.- Handler global : mapping centralisé exceptions → réponses.
- Logs structurés + redaction des secrets + pas de body brut en production.
- Timeouts configurés partout + transformation des erreurs de dépendances en 503 cohérents.