## 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.
4.4 KiB
Gestion des erreurs — Projet Batch-cooking
Documentation du contrat d'erreurs partagé entre
apps/apietapps/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) etApiErrorResponse(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,HttpErrormé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 chaqueErrorCodeen libellé affichable, avec un système de locale (fraujourd'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 :
- Ajouter le membre dans
ErrorCode. - Le lever via
new HttpError(status, ErrorCode.XXX, "message dev-facing"). - Ajouter sa traduction dans
ErrorMessageService.LABELS.fr.
Côté API (apps/api)
lib/http-error.ts—HttpError: erreur typée portantstatus(code HTTP) etcode(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'appelererrorHandlerService.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èveApiError(porteur destatus,code,fieldErrors) pour toute réponse non-2xx.services/error-message.service.ts—ErrorMessageService: associe chaqueErrorCodeà 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) attrapentApiError, récupèrenterr.code, et appellenterrorMessageService.getLabel(err.code)pour l'afficher — jamaiserr.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.