09 - Erreurs : un format que tes clients vont adorer
Ce que tu vas apprendre
- Le format RFC 7807 Problem Détails et ses cinq champs
- La différence entre codes HTTP et codes d'erreur métier
- Comment gerer l'internationalisation des messages d'erreur
- Pourquoi documenter ses erreurs change la vie de tout le monde
Prerequisites
Avoir lu les articles précédents sur les verbes HTTP et les status codes. Savoir ce qu'est un middleware dans un framework web (Express, Elysia, Fastify).
J'ai travaille sur une API ou chaque endpoint renvoyait ses erreurs dans un format différent. Un endpoint retournait { error: "not found" }, un autre { message: "Not Found", code: 404 }, un troisieme { errors: ["Not found"] }. Le front devait gerer trois parsers différents pour la meme information. C'etait un cauchemar.
Le problème des erreurs non standardisees
Quand tu n'as pas de format d'erreur uniforme, chaque développeur invente le sien. Le front finit par écrire du code defensif partout :
typescript// Le front qui essaie de comprendre l'erreur
const message = error.message || error.error || error.errors?.[0] || "Erreur inconnue";
C'est fragile, c'est penible, et ca casse des qu'un nouveau dev ajoute un endpoint avec un quatrieme format.
RFC 7807 Problem Détails
La RFC 7807 definit un format standard pour les erreurs HTTP. Cinq champs, pas plus :
json{
"type": "https://api.paltemps.fr/errors/validation-failed",
"title": "Validation Failed",
"status": 422,
"detail": "Le champ 'email' ne contient pas une adresse valide.",
"instance": "/users/42"
}
- type : une URI qui identifié le type d'erreur. Idealement un lien vers la doc.
- title : un résumé lisible par un humain. Toujours le meme pour un meme
type. - status : le code HTTP. Redondant avec le header, mais pratique quand tu logues le body.
- détail : le message spécifique a cette occurrence. C'est ici que tu mets les détails.
- instance : l'URI de la ressource concernee (optionnel mais utile pour le debug).
Le Content-Type de la réponse doit etre application/problem+json.
Codes HTTP vs codes d'erreur métier
Les codes HTTP couvrent les cas génériques : 400 (requête invalide), 404 (pas trouve), 422 (validation), 500 (erreur serveur). Mais ils ne suffisent pas pour distinguer tes erreurs métier.
Exemple : un 422 peut signifier "email invalide" ou "quota dépassé". Le code HTTP est le meme. C'est le champ type qui fait la différence :
typescript// Erreur de validation
{
type: "https://api.example.com/errors/invalid-email",
title: "Invalid Email",
status: 422,
detail: "L'adresse 'toto@' n'est pas un email valide."
}
// Erreur de quota
{
type: "https://api.example.com/errors/quota-exceeded",
title: "Quota Exceeded",
status: 422,
detail: "Tu as atteint la limite de 100 projets."
}
Le front peut brancher sa logique sur le type plutot que sur le status. Beaucoup plus precis.
L'enveloppe d'erreur etendue
La RFC 7807 permet d'ajouter des champs supplementaires. Pour les erreurs de validation, j'ajoute souvent un tableau errors :
json{
"type": "https://api.example.com/errors/validation-failed",
"title": "Validation Failed",
"status": 422,
"detail": "2 champs sont invalides.",
"errors": [
{ "field": "email", "message": "Format invalide", "code": "INVALID_FORMAT" },
{ "field": "age", "message": "Doit etre positif", "code": "MIN_VALUE" }
]
}
Le champ code dans chaque erreur est un identifiant stable que le front peut utiliser pour afficher des messages traduits. "INVALID_FORMAT" ne changera jamais, meme si tu reformules le message cote serveur.
Internationalisation des messages
Deux approches. La première : le serveur renvoie le message dans la langue demandee via le header Accept-Language :
typescriptapp.use((req, res, next) => {
const lang = req.headers["accept-language"]?.split(",")[0] || "en";
req.locale = lang.startsWith("fr") ? "fr" : "en";
next();
});
La deuxieme (que je préféré) : le serveur renvoie un code machine (INVALID_FORMAT) et c'est le front qui traduit. Le serveur n'a pas a connaître les traductions. Le front a deja un système i18n (i18next, vue-i18n, etc.), autant l'utiliser.
typescript// Cote front
const errorMessages: Record<string, Record<string, string>> = {
fr: { INVALID_FORMAT: "Le format est invalide", MIN_VALUE: "La valeur minimum n'est pas respectee" },
en: { INVALID_FORMAT: "Invalid format", MIN_VALUE: "Minimum value not met" }
};
Un middleware d'erreur centralise
Ne gere pas les erreurs dans chaque route. Un seul middleware qui attrape tout :
typescriptclass AppError extends Error {
constructor(
public type: string,
public title: string,
public status: number,
public detail: string
) {
super(detail);
}
}
// Middleware Express
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof AppError) {
return res.status(err.status).json({
type: `https://api.example.com/errors/${err.type}`,
title: err.title,
status: err.status,
detail: err.detail,
instance: req.originalUrl
});
}
// Erreur non prevue
console.error(err);
res.status(500).json({
type: "https://api.example.com/errors/internal",
title: "Internal Server Error",
status: 500,
detail: "Une erreur inattendue s'est produite."
});
});
L'avantage : chaque route lance juste throw new AppError(...) et le middleware fait le reste.
Documenter ses erreurs
Chaque type URI devrait pointer vers une page de documentation qui explique :
- Ce que l'erreur signifie
- Ce que le client peut faire pour la corriger
- Un exemple de requête qui provoque cette erreur
C'est un investissement qui se rentabilise vite. Moins de tickets support, moins de questions sur Slack, moins de frustration.
Résumé
- Utilise RFC 7807 Problem Détails pour toutes tes erreurs :
type,title,status,detail,instance - Ajoute des codes d'erreur métier dans le champ
typepour distinguer les erreurs qui partagent le meme status HTTP - Pour l'i18n, renvoie des codes machine et laisse le front traduire
- Centralise la gestion d'erreur dans un middleware unique
- Documente chaque type d'erreur avec des exemples
Article précédent : Validation des donnees Article suivant : Authentification