batchCooking/specs/frontend-architecture.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

5.3 KiB

Architecture frontend — Projet Batch-cooking

Documentation de l'organisation d'apps/web : structure des dossiers, routing, gestion des erreurs, et conventions de style (SCSS/theming).


Structure des dossiers

apps/web/src/
├── api/
│   └── client.ts            # ApiClient — appels fetch vers l'API (voir error-handling.md)
├── services/
│   └── error-message.service.ts  # ErrorMessageService — libellés d'erreur i18n
├── features/
│   └── auth/                # tout ce qui concerne l'authentification
│       ├── AuthContext.tsx       # état global (profil connecté, login/signup/logout)
│       ├── RequireAuth.tsx       # garde de route : redirige vers /login si non connecté
│       ├── RedirectIfAuthenticated.tsx  # garde de route inverse (pour /login, /signup)
│       └── auth-form.scss        # styles partagés par LoginPage et SignupPage
├── pages/
│   ├── LoginPage.tsx / .scss (via auth-form.scss, partagé)
│   ├── SignupPage.tsx / .scss (via auth-form.scss, partagé)
│   └── HomePage.tsx + HomePage.scss
├── styles/
│   ├── _theme.scss          # tokens de design (couleurs, espacements, typographie)
│   └── global.scss          # reset minimal + import du theme — importé une seule fois (main.tsx)
├── lib/
│   └── zod-errors.ts        # utilitaire : erreurs zod → { champ: message }
├── App.tsx                  # table de routes
└── main.tsx                 # point d'entrée : providers (Router, AuthProvider) + import du CSS global

Règle de placement des styles : un style spécifique à un seul composant/page vit dans un fichier .scss au même niveau que ce composant (HomePage.tsx + HomePage.scss). Un style partagé par plusieurs composants d'une même feature vit dans le dossier de la feature (features/auth/auth-form.scss, utilisé par LoginPage et SignupPage). Seuls le reset et les tokens globaux vivent dans styles/.


Routing et gardes d'authentification

flowchart TB
  START(("Visite de l'app"))
  CHECK{"AuthProvider :<br/>GET /auth/me"}
  START --> CHECK

  CHECK -->|"200 (session valide)"| AUTHED["user défini"]
  CHECK -->|"401 (pas de session)"| ANON["user = null"]

  AUTHED --> ROUTE_HOME["/ → HomePage"]
  AUTHED --> ROUTE_LOGIN_A["/login ou /signup"]
  ROUTE_LOGIN_A -->|"RedirectIfAuthenticated"| ROUTE_HOME

  ANON --> ROUTE_HOME_A["/"]
  ROUTE_HOME_A -->|"RequireAuth"| ROUTE_LOGIN["/login"]
  ANON --> ROUTE_LOGIN2["/login ou /signup → rendu normal"]
  • AuthContext (features/auth/AuthContext.tsx) appelle GET /auth/me une seule fois au montage pour restaurer la session depuis le cookie httpOnly — c'est ce qui permet à un rechargement de page de garder l'utilisateur connecté.
  • RequireAuth et RedirectIfAuthenticated sont deux gardes de route (react-router-dom) qui lisent cet état : la première protège /, la seconde protège /login et /signup (redirige un utilisateur déjà connecté vers /). Les deux affichent null tant que la vérification initiale est en cours, pour éviter un flash de contenu suivi d'une redirection.

Client API et gestion des erreurs

Voir error-handling.md pour le détail du contrat d'erreurs partagé avec l'API. En résumé côté frontend :

  • ApiClient (api/client.ts) — classe avec instance unique exportée (apiClient), enveloppe fetch avec credentials: "include" (requis pour que le cookie de session httpOnly parte/revienne, l'API et le web étant sur des origines différentes). Lève ApiError (porteuse du code d'erreur) pour toute réponse non-2xx.
  • ErrorMessageService (services/error-message.service.ts) — traduit un code d'erreur en libellé affichable, avec support de locale (fr uniquement pour l'instant).

SCSS et theming

  • sass (Dart Sass) est utilisé via le support natif de Vite — aucune config supplémentaire needed au-delà d'avoir le package installé (vite.config.ts fixe juste l'API moderne de Sass pour éviter un warning de dépréciation).
  • styles/_theme.scss — tokens de design exposés en custom properties CSS sur :root (--color-primary, --space-md, etc.), pas en simples variables SCSS : ça les rend disponibles au runtime, pas seulement à la compilation — ce qui permettrait un futur switch de thème (ex. mode sombre) en redéfinissant juste ces variables, sans reconstruire les feuilles de style. Toute nouvelle règle CSS doit référencer var(--token), jamais une couleur/valeur en dur.
  • styles/global.scss — importé une seule fois, dans main.tsx. Contient uniquement le reset minimal et l'import du thème (@use "./theme"). Rien de spécifique à une page/un composant n'y va.
  • Les tokens étant des custom properties CSS (pas des variables Sass), ils sont disponibles globalement au runtime dès que global.scss a été chargé une fois — un fichier .scss de composant/page les consomme directement via var(--token), sans avoir besoin de @use le partiel theme (ce serait un import sans effet, puisqu'aucun symbole Sass n'en est consommé). Chaque fichier documente en commentaire à quoi correspond chaque règle un peu non-triviale.