No description
Find a file
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
.github/workflows API: signup/login (profile creation + JWT auth) (#4) 2026-08-16 13:45:23 +02:00
apps Centralize error handling (shared codes + API/client services), code quality pass 2026-08-16 15:44:09 +02:00
packages/shared Centralize error handling (shared codes + API/client services), code quality pass 2026-08-16 15:44:09 +02:00
specs Centralize error handling (shared codes + API/client services), code quality pass 2026-08-16 15:44:09 +02:00
.dockerignore Add Docker packaging for local functional review (api + web) (#5) 2026-08-16 14:13:51 +02:00
.env.example Add Docker packaging for local functional review (api + web) (#5) 2026-08-16 14:13:51 +02:00
.gitignore Add specs + Prisma schema for the documented data model (#3) 2026-08-16 12:23:59 +02:00
.nvmrc Scaffold generic pnpm monorepo (api + web + shared) (#1) 2026-08-16 10:55:13 +02:00
biome.json Scaffold generic pnpm monorepo (api + web + shared) (#1) 2026-08-16 10:55:13 +02:00
docker-compose.yml Add Docker packaging for local functional review (api + web) (#5) 2026-08-16 14:13:51 +02:00
package.json API: signup/login (profile creation + JWT auth) (#4) 2026-08-16 13:45:23 +02:00
pnpm-lock.yaml Centralize error handling (shared codes + API/client services), code quality pass 2026-08-16 15:44:09 +02:00
pnpm-workspace.yaml Scaffold generic pnpm monorepo (api + web + shared) (#1) 2026-08-16 10:55:13 +02:00
README.md Centralize error handling (shared codes + API/client services), code quality pass 2026-08-16 15:44:09 +02:00
tsconfig.base.json Scaffold generic pnpm monorepo (api + web + shared) (#1) 2026-08-16 10:55:13 +02:00

batchCooking

Structure

Monorepo pnpm workspaces :

  • apps/api — backend Express/TypeScript (squelette générique : healthcheck, config env, Prisma non modélisé, tests Mocha + Cucumber/BDD)
  • apps/web — frontend React/Vite/TypeScript, prêt à être embarqué par Capacitor plus tard. Page de connexion/inscription en place ; le reste est encore un squelette générique.
  • packages/shared — code partagé entre api et web : schémas zod (signupSchema, loginSchema), types (SafeUserProfile), et le contrat d'erreurs (ErrorCode, ApiErrorResponse, voir specs/error-handling.md) — même règles des deux côtés, pas de risque de dérive entre front et back. A un vrai build (tscdist/, voir son package.json) : consommé en JS compilé par l'API (runtime Node pur, pas de transpilation à la volée) comme par le web (Vite).

Prérequis

  • Node.js 22 (voir .nvmrc)
  • pnpm 10 (corepack enable puis corepack use pnpm@10.12.4, ou installation manuelle)
  • Docker (pour Postgres en local)

Installation

pnpm install
cp .env.example .env
cp apps/api/.env.example apps/api/.env
cp apps/web/.env.example apps/web/.env

Puis édite ces deux .env pour renseigner de vrais POSTGRES_USER/POSTGRES_PASSWORD (et la DATABASE_URL correspondante dans apps/api/.env) : les fichiers .env.example ne contiennent volontairement aucun identifiant réel (juste changeme), et docker-compose.yml refuse de démarrer tant que POSTGRES_USER/PASSWORD/DB ne sont pas définis dans .env — pas de valeur par défaut en dur dans les fichiers commités. Même règle pour apps/api/.env : JWT_SECRET est requis, sans défaut (génère le tien, voir le commentaire dans apps/api/.env.example).

Cypress : téléchargement du binaire

pnpm install installe le package cypress mais pas forcément son binaire (le téléchargement du .exe/binaire natif peut être ignoré selon l'environnement où pnpm install a été lancé — ex. un environnement sandboxé/CI dont le cache ne correspond pas à celui de ta machine). Si pnpm --filter web e2e échoue avec une erreur du type :

No version of Cypress is installed in: ...\AppData\Local\Cypress\Cache\...
Please reinstall Cypress by running: cypress install

lance simplement, depuis ta machine :

pnpm --filter web exec cypress install

(à faire une seule fois par machine ; le binaire est mis en cache localement, hors du repo).

Développement

# Base de données Postgres locale
docker compose up -d

# Applique le schéma (première fois / après un changement de prisma/schema.prisma)
pnpm --filter api exec prisma migrate dev

# Backend (http://localhost:3000)
pnpm dev:api

# Frontend (http://localhost:5173)
pnpm dev:web

Conflit de port possible sur 5432 : si tu as déjà un Postgres natif installé sur ta machine (service Windows, Homebrew, etc.), il peut occuper le port 5432 et intercepter les connexions à la place du conteneur Docker (symptôme : Prisma renvoie P1000: Authentication failed alors que les identifiants sont corrects). Dans ce cas, mets POSTGRES_PORT=5433 (ou autre) dans ton .env et adapte le port dans la DATABASE_URL de apps/api/.env.

Qualité / Tests

pnpm lint                  # Biome (lint + format check)
pnpm lint:fix               # Biome --write
pnpm test                   # tests unitaires/intégration (Mocha, apps/api)
pnpm --filter api test:bdd   # tests d'intégration BDD (Cucumber/Gherkin, apps/api)
pnpm --filter web e2e         # tests e2e (Cypress, démarre le serveur dev automatiquement)
pnpm build                     # build de tous les workspaces

La CI GitHub Actions (.github/workflows/ci.yml) exécute lint + tests + build sur chaque push/PR vers main, puis les tests e2e Cypress.

Cucumber (apps/api)

Tests d'intégration lisibles en Gherkin, en complément de Mocha (qui reste pour les tests unitaires purs) :

  • apps/api/features/*.feature — scénarios en Given/When/Then (health.feature sert d'exemple)
  • apps/api/features/step-definitions/*.steps.ts — implémentation des steps
  • apps/api/features/support/world.ts — contexte partagé entre les steps d'un scénario (instancie l'app Express in-process via createApp(), comme le fait déjà supertest côté Mocha — pas besoin de lancer un vrai serveur)
  • apps/api/cucumber.cjs — config (extension .cjs volontaire, voir la remarque TypeScript/ESM ci-dessous)

Pour ajouter un scénario : écrire le .feature, lancer pnpm --filter api test:bdd, implémenter les steps manquants (Cucumber affiche des snippets tout prêts pour ceux qui n'existent pas encore).

Piège TypeScript/ESM à connaître (déjà rencontré avec cypress.config.ts) : les fichiers de config d'outils tiers qui font du chargement dynamique de TS (cucumber.cjs, cypress.config.ts…) sont sensibles au "type": "module" du package.json. cucumber.cjs évite le problème pour sa propre config en étant explicitement CommonJS ; les steps/world restent en .ts ESM classique et sont chargés via tsx (NODE_OPTIONS=--import=tsx, voir le script test:bdd).

Auth (apps/api)

Inscription (création de profil + foyer) et connexion, JWT dans un cookie httpOnly.

  • POST /auth/signup{ firstName, lastName, email, password } → crée le foyer (house) et le profil (user_profiles) en une transaction, pose le cookie de session, renvoie le profil (201)
  • POST /auth/login{ email, password } → pose le cookie de session, renvoie le profil (200) ; message d'erreur volontairement générique (401) que ce soit l'email ou le mot de passe qui soit incorrect
  • POST /auth/logout — efface le cookie (204)
  • GET /auth/me — profil courant, nécessite le cookie de session (401 sinon)

Mots de passe hachés avec argon2. Le hash est indépendant du foyer : un profil crée toujours son propre foyer à l'inscription (rejoindre un foyer existant n'est pas encore implémenté).

argon2 : version pinnée à 0.31.2, pas de ^. La version 0.45.1 (dernière au moment de l'écriture) segfault au runtime sur au moins une configuration Windows — reproduit de façon stable (bash sandboxé, bash non-sandboxé, PowerShell), alors que 0.31.2 fonctionne parfaitement avec la même API. Si tu montes la version, revérifie concrètement (argon2.hash(...) dans un node -e) avant de merger, un pnpm build qui passe ne suffit pas à détecter un crash runtime.

Les tests (Mocha + Cucumber) tournent avec un coût argon2 réduit (NODE_ENV=test, voir auth.service.ts) — le coût par défaut est volontairement élevé (sécurité), ce qui rendrait la suite de tests lente/instable sinon. La CI provisionne un vrai Postgres de service (.github/workflows/ci.yml) et exécute prisma migrate deploy avant les tests.

Les tests automatisés et pnpm dev:api partagent la même base Postgres locale. Lancer pnpm test/test:bdd vide user_profiles/house (TRUNCATE ... CASCADE, voir test-support/reset-db.ts) — si tu es en train de tester manuellement à la main (via le navigateur ou curl) contre le serveur de dev, un run de tests en parallèle efface tes données de test sans prévenir. Pas un bug, juste à savoir.

Page de connexion / inscription (apps/web)

  • src/api/client.tsApiClient (classe, instance unique exportée apiClient) : enveloppe fetch vers l'API (credentials: "include", requis pour que le cookie de session httpOnly parte/revienne — l'API et le front sont sur des origines différentes). URL configurable via VITE_API_URL (voir .env.example).
  • src/features/auth/AuthContext.tsx — état d'auth global ; appelle GET /auth/me au chargement pour restaurer la session depuis le cookie.
  • src/features/auth/RequireAuth.tsx / RedirectIfAuthenticated.tsx — gardes de route (react-router-dom) : / exige d'être connecté, /login et /signup redirigent vers / si on l'est déjà.
  • src/pages/{Login,Signup,Home}Page.tsx — validation client instantanée via les schémas zod partagés (packages/shared), erreurs API traduites via ErrorMessageService (voir ci-dessous).

Détail de l'organisation complète (dossiers, routing, SCSS/theming) : specs/frontend-architecture.md.

Tests Cypress (apps/web/cypress/e2e/) : smoke.cy.ts + auth.cy.ts mockent l'API via cy.intercept plutôt que de dépendre d'un vrai backend — le job e2e de la CI ne provisionne pas de Postgres/API, seulement le serveur de dev Vite. Le comportement réel de l'API est couvert par les suites Mocha/Cucumber d'apps/api (contre une vraie base).

Gestion des erreurs (API ↔ web)

Contrat d'erreurs partagé via packages/shared (ErrorCode, ApiErrorResponse) : l'API renvoie toujours { code, message, details? } (message en anglais, dev-facing — jamais affiché tel quel), et le client traduit code en libellé français via ErrorMessageService (apps/web/src/services/error-message.service.ts). Côté API, ErrorHandlerService (apps/api/src/services/error-handler.service.ts) centralise la transformation de toute erreur levée en réponse HTTP conforme.

Détail complet (schéma, exemples, comment ajouter un nouveau code d'erreur) : specs/error-handling.md.