batchCooking/specs/error-handling.md
Nicolas e9d94ff5f9 Centralize error handling (shared codes + API/client services), code quality pass
## Error handling

Requested: a centralized error-handling service on the API, custom error
codes shared across apps, and a client-side error service for i18n labels.

- packages/shared/src/errors/error-codes.ts — ErrorCode enum + ApiErrorResponse
  contract. Single source of truth: neither side hardcodes a raw error string
  the other has to guess at.
- apps/api: HttpError now carries an ErrorCode (not just a message).
  ErrorHandlerService (new) centralizes every "how do we turn a thrown error
  into an HTTP response" decision — app.ts's error middleware is now a thin
  adapter calling into it. API messages reverted to English/dev-facing (they
  were French from an earlier pass) since user-facing text is now generated
  client-side from the code.
- apps/web: ApiClient (class, singleton instance) throws ApiError carrying
  the code. ErrorMessageService (new) maps every ErrorCode to a localized
  label, structured with a Locale type from the start (only "fr" exists, but
  adding a language later is "add a locale to the map", not "hunt down every
  hardcoded string"). LoginPage/SignupPage now display
  errorMessageService.getLabel(err.code), never err.message directly.
- Tests strengthened to assert on `code`, not just HTTP status (Mocha +
  Cucumber, new "the response error code should be" step). Cypress mocks
  updated to the new {code, message} response shape.

## Code quality pass

Per explicit feedback: heavy JSDoc on every interface/type/class/function/
method/member touched in this PR, explicit public/private visibility on
every class member (ApiClient, ErrorMessageService, ErrorHandlerService,
HttpError), no HTML/logic mixing (styling extracted out of components
entirely, never inline).

ApiClient/ErrorMessageService were initially written as static-only classes;
switched to instance-based singletons (matching ErrorHandlerService's
existing pattern) after Biome's noStaticOnlyClass rule flagged the
static-only shape as an anti-pattern — same "class with visibility
modifiers" outcome, without fighting the linter.

## SCSS + theming

- apps/web/src/styles/_theme.scss — design tokens as CSS custom properties
  on :root (colors, spacing, typography), not plain Sass variables — makes
  them available at runtime, not just compile time, so a future theme
  switch (e.g. dark mode) is "redefine these variables" rather than
  rebuilding stylesheets.
- apps/web/src/styles/global.scss replaces the old single index.css:
  reset + theme import only, loaded once from main.tsx.
- Per-page/component styles colocated (HomePage.tsx + HomePage.scss);
  styles shared by multiple pages within one feature live in that feature's
  folder (features/auth/auth-form.scss, used by both Login/SignupPage) —
  not duplicated per page, not dumped in the global stylesheet either.
- Component-level .scss files intentionally don't `@use` the theme
  partial: they only consume CSS custom properties (global at runtime via
  global.scss), not Sass-level symbols, so importing it would do nothing —
  documented inline rather than left as a silently-redundant import.
- vite.config.ts opts into Sass's modern compiler API to silence a
  legacy-js-api deprecation warning on every build.

## specs/ updates

- New specs/error-handling.md — the ErrorCode/ApiErrorResponse contract,
  both services, with a flow diagram.
- New specs/frontend-architecture.md — apps/web folder structure, routing/
  auth-guard flow, SCSS/theming conventions.
- specs/batch-cooking-architecture.md links to both (original doc content
  otherwise untouched — it's the user's own hand-authored source doc).

## Verification

Full lint/mocha/cucumber/build green. Manually re-verified the whole auth
flow in a real browser against native dev servers (not just the automated
suites): signup, the EMAIL_ALREADY_IN_USE → "Cet email est déjà utilisé"
translation end-to-end (confirmed the raw API response carries the English
dev message + code, and the UI shows the French label), wrong-password
INVALID_CREDENTIALS → its label, and confirmed the theme tokens actually
apply (computed button background-color matches --color-primary, card
max-width matches the token value) rather than trusting the build succeeding.
2026-08-16 15:44:09 +02:00

4.4 KiB

Gestion des erreurs — Projet Batch-cooking

Documentation du contrat d'erreurs partagé entre apps/api et apps/web.


Vue d'ensemble

Trois 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 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.
  • apps/apiErrorHandlerService — centralise la traduction de n'importe quelle erreur levée (validation zod, HttpError métier, erreur inattendue) en { status, body } conforme au contrat. Le middleware d'erreur d'Express (app.ts) ne fait qu'appeler ce service.
  • apps/webErrorMessageService — centralise la traduction de chaque ErrorCode en libellé affichable, avec un système de locale (fr aujourd'hui, extensible). Les composants n'écrivent jamais de texte d'erreur en dur.
flowchart LR
  subgraph API["apps/api"]
    THROW["Route / service<br/>throw new HttpError(status, code, message)"]
    EHS["ErrorHandlerService.handle()"]
    THROW --> EHS
  end

  EHS -->|"JSON: { code, message, details? }"| HTTP["Réponse HTTP"]

  subgraph WEB["apps/web"]
    CLIENT["ApiClient<br/>lève ApiError(status, code, ...)"]
    EMS["ErrorMessageService.getLabel(code)"]
    UI["Composant (LoginPage, SignupPage...)"]
    CLIENT --> EMS --> UI
  end

  HTTP --> CLIENT

  SHARED[("packages/shared<br/>ErrorCode, ApiErrorResponse")]
  SHARED -. contrat .-> THROW
  SHARED -. contrat .-> CLIENT
  SHARED -. contrat .-> EMS

  style SHARED fill:none,stroke:#888,stroke-width:1px

Le contrat (packages/shared/src/errors/error-codes.ts)

enum ErrorCode {
  VALIDATION_ERROR,
  EMAIL_ALREADY_IN_USE,
  INVALID_CREDENTIALS,
  NOT_AUTHENTICATED,
  NOT_FOUND,
  INTERNAL_ERROR,
}

interface ApiErrorResponse {
  code: ErrorCode;
  message: string;   // anglais, dev-facing — jamais affiché tel quel côté UI
  details?: Record<string, string[] | undefined>; // uniquement pour VALIDATION_ERROR
}

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.

Pour ajouter un nouveau cas d'erreur :

  1. Ajouter le membre dans ErrorCode.
  2. Le lever via new HttpError(status, ErrorCode.XXX, "message dev-facing").
  3. Ajouter sa traduction dans ErrorMessageService.LABELS.fr.

Côté API (apps/api)

  • lib/http-error.tsHttpError : 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.
  • services/error-handler.service.tsErrorHandlerService : 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.
  • app.ts — le middleware d'erreur final d'Express ne fait qu'appeler errorHandlerService.handle(err) et renvoyer le résultat ; aucune logique de mapping n'y vit directement.

Côté Web (apps/web)

  • api/client.tsApiClient : lève ApiError (porteur de status, code, fieldErrors) pour toute réponse non-2xx.
  • services/error-message.service.tsErrorMessageService : associe chaque ErrorCode à un libellé, par locale (Record<Locale, Record<ErrorCode, string>>). Une seule langue existe aujourd'hui (fr), mais la structure est prête pour en ajouter une deuxième sans toucher aux composants.
  • 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 déjà des 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 : ces messages ne quittent jamais le navigateur.