batchCooking/specs/frontend-architecture.md
Nicolas 1cedb25d74 Address review: no .d.ts, express-tools package, faker fixtures, numeric codes, real i18n lib
Five explicit review points, addressed on this same PR branch (not a new
PR) per updated preference.

## No .d.ts files in the codebase

- apps/web: vite-env.d.ts removed — its /// <reference types="vite/client" />
  is replaced by "types": ["vite/client"] in tsconfig.app.json, same effect.
- apps/api: src/types/express.d.ts renamed to express-request.augment.ts —
  `declare global` module augmentation works identically in a plain .ts
  file as long as it has a top-level import (making it a module); the
  .d.ts extension wasn't doing anything for us here.

## packages/express-tools — separate package for Express tooling

Moved HttpError and ErrorHandlerService out of apps/api into a new
workspace package, plus a new createErrorMiddleware() factory (the actual
Express 4-arg error-handling middleware, previously inlined in app.ts).
apps/api now just consumes @batch-cooking/express-tools. Has a real build
(tsc -> dist/, same pattern as packages/shared) — required for the same
reason shared needed one: apps/api's Docker image runs plain `node
dist/server.js`, no tsx. apps/api/Dockerfile updated to COPY the new
package's dist alongside shared's.

## faker.js for test fixtures

apps/api/test/auth.test.ts: replaced the hardcoded "Nicolas
Lefevre"/nicolas@example.com fixture (looked like real user data) with
@faker-js/faker, generated fresh per test via buildSignupPayload().
features/step-definitions/auth.steps.ts: fakerized the filler
firstName/lastName/password used for background state the scenarios
don't actually read.

Deliberately did NOT fakerize the literal example values inside
auth.feature itself (alice@example.com etc.) — those are the readable,
illustrative Gherkin examples that are the whole point of BDD scenarios,
not real PII, and randomizing them would make the scenarios harder to
read for no real gain. Flagged this reasoning in the README in case that
call should go the other way.

Caught a real bug while wiring this up: faker.internet.email() sometimes
capitalizes parts of the address, but signupSchema/loginSchema normalize
emails to lowercase — the test fixture needs to match what's actually
stored, so buildSignupPayload() lowercases the generated email too.
Found by actually running the suite repeatedly, not just once.

## ErrorCode: numeric enum, zero hardcoded values

packages/shared/src/errors/error-codes.ts: ErrorCode is now a numeric
enum (4000 VALIDATION_ERROR, 4001 EMAIL_ALREADY_IN_USE, 4010
INVALID_CREDENTIALS, 4011 NOT_AUTHENTICATED, 4040 NOT_FOUND, 5000
INTERNAL_ERROR — grouped by family like HTTP status codes).

Audited and fixed every place that hardcoded a raw code value instead of
referencing the enum: ApiClient's fallback (`"INTERNAL_ERROR" as
ErrorCode` — would no longer even type-check once the enum went numeric,
which is exactly the point), and the Cypress mock bodies (now import
ErrorCode from @batch-cooking/shared instead of typing the string).

Cucumber's "the response error code should be {string}" step still takes
the *name* in the .feature file (readable: "EMAIL_ALREADY_IN_USE") and
resolves it to the real numeric value via ErrorCode[name] — TypeScript's
reverse enum mapping — before comparing, so the Gherkin stays readable
without the step hardcoding a number either.

## Real i18n library (i18next), not a hand-rolled label map

apps/web: added i18next + react-i18next. New locales/fr/translation.json
holds every user-facing string — not just error labels (errors.*), but
the login/signup/home pages' labels, buttons and headings too
(auth.login.*, auth.signup.*, home.*) — via useTranslation()/t() in each
page. ErrorMessageService no longer owns its own label map; it converts
the numeric ErrorCode to its enum member name and delegates the actual
lookup to i18next (errors.<MEMBER_NAME>). Adding a language is now
"add a locale file", not a code change anywhere.

## specs/ and README updated

specs/error-handling.md and specs/frontend-architecture.md rewritten for
the new package, numeric codes, and i18next. New "i18n" and "no .d.ts"
sections. README covers the same, plus a note on the faker.js scope
decision (feature-file literals excluded, on purpose).

## Verification

Full lint/mocha (x3 runs)/cucumber/build green. Re-verified
express-tools' extraction against a real risk (not just tsc passing):
ran `node dist/server.js` standalone (mirrors the Docker runtime, no
tsx) and hit /health, a 404 (confirmed numeric code 4040 over the wire),
and a real signup + duplicate-email 409 (confirmed numeric 4001). Then
re-verified the full pipeline in a real browser against native dev
servers: signup, EMAIL_ALREADY_IN_USE -> i18next -> "Cet email est déjà
utilisé" end-to-end, home page i18next interpolation
({{firstName}}/{{lastName}}) rendering correctly.
2026-08-16 16:22:29 +02:00

145 lines
7 KiB
Markdown

# 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)
├── i18n/
│ └── i18n.ts # config i18next, importé une fois (main.tsx) pour son effet de bord
├── locales/
│ └── fr/translation.json # libellés français (errors.*, auth.*, home.*)
├── services/
│ └── error-message.service.ts # ErrorMessageService — code d'erreur → clé i18next
├── 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) + imports i18n/CSS globaux
```
**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
```mermaid
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](./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`) — convertit un `code`
d'erreur numérique en clé de traduction, résolue via i18next.
---
## i18n (internationalisation)
**i18next** + **react-i18next** — pas de solution maison : tout le texte affiché
(libellés de formulaire, boutons, messages d'erreur) vient de fichiers de locale
JSON, jamais codé en dur dans un composant.
- `i18n/i18n.ts` — initialise l'instance i18next (langue par défaut `fr`), importé
une seule fois pour son effet de bord dans `main.tsx`, avant le premier rendu.
- `locales/fr/translation.json` — toutes les chaînes françaises, organisées par
namespace : `errors.*` (voir [error-handling.md](./error-handling.md)),
`auth.login.*` / `auth.signup.*`, `home.*`.
- Dans un composant : `const { t } = useTranslation(); t("auth.login.title")`.
- Ajouter une langue : créer `locales/<lng>/translation.json` avec les mêmes clés,
ajouter `resources.<lng>` dans `i18n/i18n.ts` — aucun composant à toucher.
---
## Note sur les fichiers `.d.ts`
Aucun fichier `.d.ts` écrit à la main dans `apps/web` : le
`/// <reference types="vite/client" />` généré par défaut par Vite (habituellement
`vite-env.d.ts`) est remplacé par `"types": ["vite/client"]` dans
`tsconfig.app.json` — même effet (typage de `import.meta.env`, imports d'assets),
sans fichier dédié.
Même logique côté `apps/api` : l'augmentation du type `Express.Request` (pour
`req.userProfile`) vit dans `src/types/express-request.augment.ts`, un fichier
`.ts` classique (pas `.d.ts`) — une augmentation `declare global` fonctionne
identiquement dans les deux, tant que le fichier a un import qui en fait un module.
---
## 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.