# Gestion des erreurs — Projet Batch-cooking > Documentation du contrat d'erreurs partagé entre `apps/api` et `apps/web`. --- ## Vue d'ensemble Quatre pièces travaillent ensemble pour que **toute** erreur, du serveur jusqu'à l'affichage utilisateur, passe par un chemin unique et prévisible : - **`packages/shared`** — le contrat : `ErrorCode` (énumération **numérique** de tous les codes d'erreur métier) et `ApiErrorResponse` (forme JSON de toute réponse d'erreur de l'API). Ni l'API ni le web ne définissent leur propre liste de codes, et aucune valeur n'est jamais codée en dur ailleurs (toujours `ErrorCode.XXX`, jamais un nombre/une chaîne littérale). - **`packages/express-tools`** — package séparé pour l'outillage Express générique (réutilisable par n'importe quel service Express du monorepo, pas seulement `apps/api`) : `HttpError`, `ErrorHandlerService`, `createErrorMiddleware`. - **`apps/api`** — consomme `express-tools` : lève des `HttpError`, le middleware d'erreur final n'est qu'un appel à `createErrorMiddleware(errorHandlerService)`. - **`apps/web` → `ErrorMessageService`** — associe chaque `ErrorCode` à une clé de traduction, résolue via **i18next** (fichiers de locale sous `src/locales/`). Les composants n'écrivent jamais de texte d'erreur en dur. ```mermaid flowchart LR subgraph TOOLS["packages/express-tools"] HTTPERR["HttpError"] EHS["ErrorHandlerService.handle()"] MW["createErrorMiddleware()"] end subgraph API["apps/api"] THROW["Route / service
throw new HttpError(status, code, message)"] THROW --> EHS MW -->|"app.use(...)"| EHS end EHS -->|"JSON: { code, message, details? }"| HTTP["Réponse HTTP"] subgraph WEB["apps/web"] CLIENT["ApiClient
lève ApiError(status, code, ...)"] EMS["ErrorMessageService.getLabel(code)"] I18N["i18next
locales/fr/translation.json"] UI["Composant (LoginPage, SignupPage...)"] CLIENT --> EMS --> I18N --> UI end HTTP --> CLIENT SHARED[("packages/shared
ErrorCode (numérique), ApiErrorResponse")] SHARED -. contrat .-> THROW SHARED -. contrat .-> CLIENT SHARED -. contrat .-> EMS style SHARED fill:none,stroke:#888,stroke-width:1px style TOOLS fill:none,stroke:#888,stroke-width:1px ``` --- ## Le contrat (`packages/shared/src/errors/error-codes.ts`) ```ts enum ErrorCode { VALIDATION_ERROR = 4000, EMAIL_ALREADY_IN_USE = 4001, INVALID_CREDENTIALS = 4010, NOT_AUTHENTICATED = 4011, NOT_FOUND = 4040, INTERNAL_ERROR = 5000, } interface ApiErrorResponse { code: ErrorCode; message: string; // anglais, dev-facing — jamais affiché tel quel côté UI details?: Record; // uniquement pour VALIDATION_ERROR } ``` **Codes numériques, groupés par famille** (comme les codes HTTP) : `4000`–`4099` validation, `4010`–`4019` authentification, `4040`–`4049` ressource introuvable, `5000`–`5099` interne. Le numéro donne une indication de la catégorie même sans regarder l'enum. **Règle** : `message` est destiné aux logs/au débogage (toujours en anglais, jamais localisé). Le texte affiché à l'utilisateur vient **toujours** de `ErrorMessageService.getLabel(code)` côté client, jamais de `message` directement. Et **aucune valeur `ErrorCode` n'est jamais écrite en dur** (ni en nombre, ni en chaîne) — toujours une référence `ErrorCode.XXX`, y compris dans les tests/mocks. Pour ajouter un nouveau cas d'erreur : 1. Ajouter le membre dans `ErrorCode`, dans la bonne plage numérique. 2. Le lever via `new HttpError(status, ErrorCode.XXX, "message dev-facing")`. 3. Ajouter sa traduction dans **chaque** fichier `apps/web/src/locales/*/translation.json`, sous `errors.XXX`. --- ## `packages/express-tools` — outillage Express générique Séparé d'`apps/api` volontairement : ce sont des briques génériques (n'importe quel service Express du monorepo pourrait les utiliser), pas de logique métier. - **`http-error.ts`** — `HttpError` : erreur typée portant `status` (code HTTP) et `code` (`ErrorCode`). C'est ce que lèvent les routes/services au lieu de construire une réponse HTTP à la main. - **`error-handler.service.ts`** — `ErrorHandlerService` : un seul point qui sait transformer n'importe quelle erreur JS (`ZodError`, `HttpError`, n'importe quoi d'autre) en `{ status, body }`. Le cas générique (`INTERNAL_ERROR`, 500) logue l'erreur côté serveur sans jamais exposer de détail interne au client. - **`error-middleware.ts`** — `createErrorMiddleware(service)` : construit le middleware d'erreur Express (signature à 4 arguments) à partir du service — adaptateur fin, aucune logique de mapping n'y vit. Build réel (`tsc` → `dist/`, comme `packages/shared`) : consommé en JS compilé, pas en TS brut — voir la note dans [frontend-architecture.md](./frontend-architecture.md#note-sur-les-fichiers-dts) sur pourquoi ça compte pour un runtime Node pur (Docker). ## Côté API (`apps/api`) - **`app.ts`** — le middleware d'erreur final est `app.use(createErrorMiddleware(errorHandlerService))` ; aucune logique de mapping n'y vit directement, tout est dans `express-tools`. - Les modules métier (`modules/auth/auth.service.ts`, `middlewares/require-auth.ts`) importent `HttpError` depuis `@batch-cooking/express-tools` et `ErrorCode` depuis `@batch-cooking/shared`. ## Côté Web (`apps/web`) - **`api/client.ts`** — `ApiClient` : lève `ApiError` (porteur de `status`, `code`, `fieldErrors`) pour toute réponse non-2xx. - **`services/error-message.service.ts`** — `ErrorMessageService` : convertit le `ErrorCode` numérique reçu en nom de membre (`ErrorCode[code]`, ex. `4001` → `"EMAIL_ALREADY_IN_USE"`), puis délègue la traduction à **i18next** (`i18n.t(\`errors.${memberName}\`)`). N'a pas sa propre table de libellés — c'est i18next + les fichiers de locale qui la portent. - **`i18n/i18n.ts`** + **`locales/fr/translation.json`** — configuration et ressources i18next. Ajouter une langue = ajouter une entrée `resources.` pointant vers un nouveau fichier de locale, sans toucher un seul composant. - Les pages (`LoginPage`, `SignupPage`) attrapent `ApiError`, récupèrent `err.code`, et appellent `errorMessageService.getLabel(err.code)` pour l'afficher — jamais `err.message`. ## Validation côté formulaire (distincte du contrat d'erreurs API) Les schémas zod partagés (`packages/shared/src/schemas/auth.ts`) portent leurs propres messages en français, utilisés pour la validation **avant** l'appel réseau (retour instantané, aucun aller-retour serveur). C'est un mécanisme séparé du contrat `ErrorCode`/i18next : ces messages ne quittent jamais le navigateur, et ne vivent pas dans les fichiers de locale (ils sont dans `packages/shared`, consommé aussi par l'API qui ne dépend pas d'i18next).