Five more explicit review points, on the same PR branch. ## Every interface key commented Audited all 6 interfaces in the codebase. Two had partially-commented members (violates the "every key gets /** */" rule): AuthResult (apps/api/auth.service.ts) and SafeUserProfile (packages/shared) — both now fully commented. The other four (AuthTokenPayload, AuthContextValue, ErrorHandlingResult, ApiErrorResponse) were already compliant. ## Removed the Express namespace augmentation apps/api/src/types/express.d.ts (renamed to express-request.augment.ts in the last round) is gone entirely. requireAuth now attaches the authenticated profile to `res.locals.userProfile` — Express's own built-in per-request mechanism for exactly this — typed via a new AuthLocals interface and `Response<unknown, AuthLocals>`, instead of a project-wide `declare global` silently changing every Request's type whether or not it went through the middleware. ## ErrorHandlerService confirmed framework-agnostic It already had zero Express import. Documented this explicitly (in the package's index.ts and the new backend-architecture.md spec) as a deliberate split: ErrorHandlerService is framework-agnostic (would work behind Fastify too), ExpressServer/createErrorMiddleware are the actual Express integration layer. ## packages/express-tools: server init + route/middleware utilities New ExpressServer class, modeled on the pattern shared as a reference (adapted, not copied 1:1 — deliberately left out the reference's custom runtime param-type-validation system, since zod already does that job in this codebase and running two parallel validation mechanisms would be redundant, not "propre"): - setupCore() — the common cors/json/cookie-parser stack - addRoute() — registers a route, warns+skips instead of silently double-registering the same method+path - addMiddleware() / mountRouter() / setErrorHandler() - listen() - .instance — the raw Express app, for supertest Also added wrapAsyncHandler() — forwards a thrown/rejected error from an async handler to next(err) automatically, removing the manual try/catch/next(err) every route needed. apps/api/src/app.ts now builds via ExpressServer (createServer(), consumed by both server.ts's .listen() and createApp()'s .instance for tests). auth.routes.ts's signup/login handlers use wrapAsyncHandler instead of manual try/catch. cookie-parser/cors moved out of apps/api's own dependencies entirely — they're express-tools' concern now. ## assertIsNever (packages/shared/src/tools/) Exhaustiveness-check helper for switch/if-chains over a union: takes a `never`-typed value and throws, so a forgotten case in a later-added union member becomes a compile error instead of a silent runtime fallthrough. Verified for real (not just written and assumed correct): wrote a throwaway switch missing a case and confirmed `tsc` rejects it with the exact expected error, then deleted the scratch file. No existing switch/if-chain over a union in the codebase yet to retrofit it into — noted as ready for when one appears (e.g. the not-yet-built batch-cooking calculation module or recipe-import pipeline). ## specs/ updated New specs/backend-architecture.md — ExpressServer, wrapAsyncHandler, the res.locals decision (with the "why not declare global" reasoning spelled out), assertIsNever. error-handling.md and frontend-architecture.md cross-link to it instead of duplicating. README covers the same, briefly. ## Verification Full lint/mocha/cucumber/build green. Re-ran `node dist/server.js` standalone (mirrors Docker, no tsx) after the ExpressServer refactor: /health, a 404 (numeric 4040), and a real signup + GET /me round trip confirming res.locals-based auth actually works at runtime, not just that tsc accepts the types.
7 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)
├── 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
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) appelleGET /auth/meune 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é.RequireAuthetRedirectIfAuthenticatedsont deux gardes de route (react-router-dom) qui lisent cet état : la première protège/, la seconde protège/loginet/signup(redirige un utilisateur déjà connecté vers/). Les deux affichentnulltant 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), enveloppefetchaveccredentials: "include"(requis pour que le cookie de session httpOnly parte/revienne, l'API et le web étant sur des origines différentes). LèveApiError(porteuse ducoded'erreur) pour toute réponse non-2xx.ErrorMessageService(services/error-message.service.ts) — convertit uncoded'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éfautfr), importé une seule fois pour son effet de bord dansmain.tsx, avant le premier rendu.locales/fr/translation.json— toutes les chaînes françaises, organisées par namespace :errors.*(voir 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.jsonavec les mêmes clés, ajouterresources.<lng>dansi18n/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é.
- Côté
apps/api, aucune augmentation de type globale n'est utilisée du tout — voir - backend-architecture.md
- le profil authentifié passe par
res.locals(mécanisme natif d'Express), pas par undeclare globalsurExpress.Request.
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.tsfixe 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érencervar(--token), jamais une couleur/valeur en dur.styles/global.scss— importé une seule fois, dansmain.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.scssa été chargé une fois — un fichier.scssde composant/page les consomme directement viavar(--token), sans avoir besoin de@usele 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.