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. |
||
|---|---|---|
| .github/workflows | ||
| apps | ||
| packages | ||
| specs | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .nvmrc | ||
| biome.json | ||
| docker-compose.yml | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.base.json | ||
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é entreapietweb: schémas zod (signupSchema,loginSchema), types (SafeUserProfile), et le contrat d'erreurs (ErrorCodenumérique,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.packages/express-tools— outillage Express générique et réutilisable :ExpressServer(init serveur, routes, middlewares),wrapAsyncHandler,HttpError,ErrorHandlerService,createErrorMiddleware— séparé d'apps/api, pas de logique métier. Détail : specs/backend-architecture.md.
packages/shared et packages/express-tools ont un vrai build (tsc → dist/,
voir leur package.json) : consommés en JS compilé, pas en TS brut — nécessaire
pour un runtime Node pur (Docker, pas de transpilation à la volée), voir la note
dans specs/frontend-architecture.md.
Prérequis
- Node.js 22 (voir
.nvmrc) - pnpm 10 (
corepack enablepuiscorepack 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 renvoieP1000: Authentication failedalors que les identifiants sont corrects). Dans ce cas, metsPOSTGRES_PORT=5433(ou autre) dans ton.envet adapte le port dans laDATABASE_URLdeapps/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.featuresert d'exemple)apps/api/features/step-definitions/*.steps.ts— implémentation des stepsapps/api/features/support/world.ts— contexte partagé entre les steps d'un scénario (instancie l'app Express in-process viacreateApp(), comme le fait déjà supertest côté Mocha — pas besoin de lancer un vrai serveur)apps/api/cucumber.cjs— config (extension.cjsvolontaire, 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"dupackage.json.cucumber.cjsévite le problème pour sa propre config en étant explicitement CommonJS ; les steps/world restent en.tsESM classique et sont chargés viatsx(NODE_OPTIONS=--import=tsx, voir le scripttest: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 incorrectPOST /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 version0.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 que0.31.2fonctionne parfaitement avec la même API. Si tu montes la version, revérifie concrètement (argon2.hash(...)dans unnode -e) avant de merger, unpnpm buildqui 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:apipartagent la même base Postgres locale. Lancerpnpm test/test:bddvideuser_profiles/house(TRUNCATE ... CASCADE, voirtest-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.ts—ApiClient(classe, instance unique exportéeapiClient) : enveloppefetchvers 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 viaVITE_API_URL(voir.env.example).src/features/auth/AuthContext.tsx— état d'auth global ; appelleGET /auth/meau 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é,/loginet/signupredirigent 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 viaErrorMessageService(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, énumération
numérique groupée par famille — 4000 validation, 401x auth, 404x not
found, 500x interne — et 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 i18next
(ErrorMessageService, apps/web/src/services/error-message.service.ts →
apps/web/src/locales/fr/translation.json). Côté API, ErrorHandlerService et
createErrorMiddleware (packages/express-tools) centralisent la transformation
de toute erreur levée en réponse HTTP conforme — aucune valeur ErrorCode codée
en dur nulle part (toujours ErrorCode.XXX, y compris dans les mocks Cypress).
Détail complet (schéma, exemples, comment ajouter un nouveau code d'erreur) : specs/error-handling.md.
Le profil authentifié (requireAuth) passe par res.locals.userProfile
(typé via AuthLocals), pas par une augmentation du namespace global Express —
voir specs/backend-architecture.md pour le détail
et le pourquoi.
packages/shared fournit aussi assertIsNever (vérification d'exhaustivité de
switch/if-chain sur une union, erreur de compilation si un cas est oublié) —
voir specs/backend-architecture.md.
i18n
i18next + react-i18next — tout le texte affiché (formulaires, boutons,
erreurs) vient de fichiers de locale JSON (apps/web/src/locales/<lng>/translation.json),
jamais codé en dur dans un composant. Une seule langue existe aujourd'hui (fr) ;
en ajouter une est une question de fichier de locale, pas de code. Détail :
specs/frontend-architecture.md.
Données de test (faker.js)
apps/api utilise @faker-js/faker pour toutes les données
de test dans test/auth.test.ts (Mocha) et le "bruit" (prénom/nom de remplissage)
des steps Cucumber — jamais de nom/email qui ressemble à une vraie personne en dur
dans un fixture. Les valeurs littérales des scénarios .feature eux-mêmes
(ex. alice@example.com) restent volontairement statiques : c'est le point des
scénarios Gherkin lisibles (exemples illustratifs conventionnels en BDD, pas des
données réelles) — seules les données de remplissage hors du texte lisible du
scénario sont générées.