Nouveau specs/dev-conventions.md — jusqu'ici ces règles n'existaient que dans l'historique de commits/PR (classes vs objets littéraux pour la logique de service, préfixe _ sur private/protected, règles Biome actives, log service, tests sans mocks de la DB, conventions git/PR...), rien de centralisé pour un futur contributeur (humain ou Claude Code). Référencé depuis README.md, section "Qualité / Tests". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
12 KiB
Conventions de développement — Projet Batch-cooking
Règles de style/architecture qui s'appliquent à tout le code du monorepo, transverses aux specs par domaine (backend-architecture.md, frontend-architecture.md, error-handling.md). Ce document répond à « comment j'écris du code ici », les autres répondent à « comment tel module est construit ».
TypeScript — style général
- Commenter le plus possible : chaque interface, type, classe, fonction,
méthode et membre de classe porte un commentaire JSDoc (
/** ... */). Le commentaire explique le pourquoi (la décision, le compromis, le piège évité), pas juste une paraphrase de la signature — la plupart des exemples de ce repo font ça abondamment, c'est la norme attendue, pas une option. - Jamais de mélange HTML/TypeScript : le JSX vit uniquement dans les
fichiers
.tsxde composant. Toute logique (appels API, validation, dérivation d'état, formatage) est extraite dans un.tsplain importé par le composant. Pas destyle={{}}inline ni de CSS-in-JS — le style va dans un fichier.scss(voir plus bas). - Modificateurs de visibilité explicites : dans toute classe, chaque
membre et méthode porte
public,privateouprotected— jamais le défaut implicite (public) de TypeScript.
Classes, jamais d'objets littéraux pour de la logique de service
Un objet qui regroupe du comportement (un logger, la gestion d'erreurs, un
client API, une résolution de libellés — dès qu'il y a un verbe, pas
seulement de la donnée) est une vraie class, jamais un objet littéral
assigné à un const :
// ❌ jamais ça, même pour un singleton sans état avec un seul appelant
export const logger = {
info(message: string) { /* ... */ },
};
// ✅ toujours ça
export class LoggerService {
public info(message: string): void { /* ... */ }
private _emit(level: LogLevel, message: string): void { /* ... */ }
}
/** Instance unique partagée — ce service est sans état, pas besoin d'une par appelant. */
export const logger = new LoggerService();
Exemples dans le repo : LoggerService (apps/api/src/lib/logger.service.ts),
ErrorHandlerService (packages/error-tools), ExpressServer
(packages/express-tools), ApiClient/ErrorMessageService (apps/web).
Un objet purement data (une table de correspondance, un objet qui implémente
une interface orientée donnée sans logique propre — ex. theMealDbAdapter,
INGREDIENT_LABELS_EN) n'est pas concerné, il reste un objet littéral.
React fonctionnel (hooks, function components, contexts) n'est pas forcé dans des classes — cette règle vise spécifiquement la logique de service, pas l'idiome React.
Préfixe _ sur tout membre private/protected
Tout champ ou méthode private/protected d'une classe porte le préfixe _
— emit → _emit, app → _app. public ne porte jamais ce préfixe.
Rétroactif : appliqué à chaque classe existante du repo (pas seulement au
nouveau code) — voir LoggerService._emit/_minSeverity,
ApiClient._request, ErrorHandlerService._fromZodError/_fromHttpError/
_fromUnknownError, ExpressServer._app/_registeredRoutes.
Élégance avant rapidité
Préférer une solution propre, bien structurée et délibérée à une solution rapide/bricolée — y compris quand le correctif rapide "marche". C'est le fil conducteur des règles ci-dessus, pas une règle séparée.
Lint & format — Biome
biome.json à la racine, un seul config pour tout le monorepo (pnpm lint /
pnpm lint:fix / pnpm format à la racine, jamais un linter par package).
Formatage : indentation 2 espaces, largeur de ligne 100, guillemets
doubles, point-virgules toujours, virgules finales partout
(trailingCommas: "all"), tri automatique des imports (assist.organizeImports).
Règles actives au-delà du preset recommended :
| Règle | Effet |
|---|---|
suspicious.noExplicitAny |
interdit any explicite (déjà dans recommended, listée ici pour la visibilité) — utiliser un type précis ou unknown + vérification |
suspicious.noConsole (allow: ["error","warn","info","debug","table","assert"]) |
interdit console.log nu ; les niveaux nommés restent autorisés, mais uniquement depuis LoggerService (voir plus bas) — le reste du code ne touche jamais console directement |
nursery.noFloatingPromises |
interdit une Promise ni await-ée, ni .then(ok, err)/.catch(), ni void-ée, ni return-ée — un useEffect qui lance un fetch fire-and-forget doit soit avoir un .catch(), soit être explicitement void navigate(...) |
noFloatingPromises est une règle type-aware (nursery, nécessite Biome
≥ 2.0) — activée directement sous rules.nursery sans passer par
linter.domains (qui allumerait tout un lot d'autres règles type-aware hors
scope, ex. noUnresolvedImports).
Pour un flottant légitime (effet de bord volontairement non attendu, ex. une
navigation après un catch qui ne peut pas échouer utilement) : void maPromise(); — jamais un // biome-ignore pour ce cas précis, seulement pour
les rares cas où la règle se trompe vraiment (voir le commentaire au-dessus de
chaque biome-ignore existant, jamais silencieux).
Logs de fonctionnement côté serveur
apps/api/src/lib/logger.service.ts — LoggerService, instance unique
export const logger = new LoggerService(). Seul endroit du code autorisé à
appeler console.* (via un biome-ignore justifié) ; partout ailleurs,
noConsole l'interdit — le but est qu'un log traverse toujours
logger.debug/info/warn/error(message, meta?), jamais un console.log nu.
- Émet une ligne JSON structurée (
timestamp,level,message, +metafusionné) — pas du texte libre, pour rester grep/parse-able pardocker logs/Portainer ou un agrégateur de logs. - Seuil filtré par
NODE_ENV:debugcomplet en développement,warn+ en test (pour ne pas alourdir la sortie de Mocha),info+ en production. apps/api/src/middlewares/request-logger.ts— une ligne par requête terminée (méthode/chemin/statut/durée), montée en tout premier dansapp.ts, avant mêmesetupCore(CORS/JSON/cookies).apps/api/src/middlewares/error-logger.ts— monté juste avantcreateErrorMiddleware; réutiliseerrorHandlerService.handle()(pur, sans effet de bord) pour classifier l'erreur avant que la réponse ne soit construite :warnpour un4xxroutinier (validation, 404, 401...),error(avec la stack) pour un5xx/une exception non prévue.
apps/web n'a pas d'équivalent aujourd'hui — noConsole s'y applique aussi,
mais rien ne journalise encore côté client (hors ce que le navigateur capture
lui-même).
SCSS
- SCSS (
sass), jamais de CSS brut ni de CSS-in-JS. - Commenter le SCSS aussi librement que le TypeScript — expliquer ce qu'une règle affecte et pourquoi, pas juste ce qu'elle fait.
- Styles globaux (reset, tokens de thème) dans une feuille globale ;
styles de page/composant colocalisés au même niveau que les autres fichiers
du composant (
LoginPage.tsx+LoginPage.scsscôte à côte). - Un fichier de tokens réutilisable (
_theme.scssou équivalent : couleurs, espacements, typographie) importé partout où nécessaire, pour qu'un re-stylage reste un changement centralisé.
i18n
Tout texte affiché (formulaires, boutons, messages d'erreur) vient d'un
fichier de locale JSON (apps/web/src/locales/<lng>/translation.json),
jamais codé en dur dans un composant — voir
frontend-architecture.md
pour le détail (i18next/react-i18next, une seule langue aujourd'hui fr).
Gestion des erreurs
Contrat complet documenté à part : error-handling.md.
En bref — ErrorCode (numérique, jamais codé en dur), HttpError
(packages/error-tools, framework-agnostic), ErrorHandlerService.handle()
comme unique point de mapping erreur → réponse HTTP, ErrorMessageService
côté web pour traduire un code en libellé affiché (jamais err.message
directement à l'utilisateur).
Tests
apps/api— Mocha + Chai, contre une vraie base Postgres isolée (.env.test, jamais la même base quepnpm dev:api), pas de mocks de la base ou des services internes. Seule exception : le premier module à parler à un vrai réseau externe (the-meal-db.ts) stubglobalThis.fetch— pas de librairie de mock HTTP dans le repo, un simplefetchréassignable suffit pour l'unique cas qui en a besoin.- Données de test générées, jamais des données réalistes en dur :
@faker-js/fakerpour tout nom/email/mot de passe de fixture — jamais un nom/email qui ressemble à une vraie personne codé en dur dans un test. apps/web— Cypress (+ Cucumber pour certains scénarios), mocke l'API viacy.interceptplutôt que de dépendre d'un vrai backend ; le comportement réel de l'API est couvert côtéapps/api. Toute valeurErrorCodeutilisée dans un mock passe parErrorCode.XXX, jamais une valeur numérique/chaîne codée en dur — même règle que le code applicatif.- Un module pur (pas de DB/réseau) est toujours testable isolément de sa
variante DB-backed — convention systématique dans ce repo :
matchXxx()pur +loadXxxCatalog()qui touche la base, jamais les deux mélangés dans la même fonction (voiringredient-matcher.ts,tech-step-matcher.ts,recipe-translation.ts).
Monorepo & packages partagés
pnpm-workspace.yaml : apps/* + packages/*. packages/shared,
packages/error-tools, packages/express-tools et packages/date-tools ont
un vrai build (tsc → dist/) et sont consommés en JS compilé, jamais en
TS brut — nécessaire pour un runtime Node pur sans transpilation à la volée
(l'image Docker de prod). Concrètement : après avoir modifié un fichier dans
un de ces packages, il faut le rebuilder (pnpm --filter <package> build, ou
juste pnpm build à la racine) pour que apps/api/apps/web voient le
changement en dev — un tsx watch/Vite ne recompile pas le package pour vous.
Séparation stricte par dépendance, pas par convention de nommage :
packages/error-tools n'importe jamais express (sinon il vivrait dans
express-tools) ; packages/express-tools dépend d'error-tools, jamais
l'inverse.
Git & Pull Requests
- Jamais de commit direct sur
main— toujours une branche de feature, même pour un correctif d'une ligne. Tout ce qui arrive surmainpasse par une PR revue (l'historique demainn'est fait que de commits de merge de PR). - Messages de commit en français, format
type(scope): description(feat,fix,style,docs,chore...), scope = le domaine touché (recipes,planning,api,layout...). Toujours un trailerCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>quand le commit vient de Claude Code. - PRs plutôt petites et scopées à un sujet — l'historique de ce repo est fait
de nombreuses PRs courtes (
feat(recipes): ...,fix(planning): ...) plutôt que de grosses PRs multi-sujets, sauf demande explicite contraire.
Variables d'environnement & secrets
- Aucun secret n'a de valeur par défaut codée en dur (
JWT_SECRET,POSTGRES_PASSWORD...) —docker-compose.yml/config/env.tséchouent bruyamment si absent, jamais un fallback devinable. .env.examplene contient que des valeurs bidon (changeme), jamais de vrai identifiant, même de dev..env(dev) et.env.test(suite Mocha) pointent vers des bases Postgres différentes —resetDatabase()fait unTRUNCATEavant chaque test, partager la base viderait les données de dev à chaque run de tests.