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

112 lines
4.4 KiB
Markdown

# 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/api` → `ErrorHandlerService`** — 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/web` → `ErrorMessageService`** — 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.
```mermaid
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`)
```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.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.
- **`services/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.
- **`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.ts`** — `ApiClient` : lève `ApiError` (porteur de `status`, `code`,
`fieldErrors`) pour toute réponse non-2xx.
- **`services/error-message.service.ts`** — `ErrorMessageService` : 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.