## 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.
112 lines
4.4 KiB
Markdown
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.
|