Files
_Assistant_Lead_Tech/knowledge/backend/risques/contracts.md
T
MaksTinyWorkshop f1b783407a docs(knowledge): capitalisation backend — intégration du triage local (mai-juin 2026)
Triage et intégration des propositions backend du buffer 95_a_capitaliser.md
(lot local RL799_V2 + app-alexandrie, mai-juin 2026), distinct de la capitalisation
remote antérieure (triage 2026-05-02).

~73 entrées intégrées sur knowledge/backend/, dont :
- patterns/auth.md : série "membrane d'auth fédérée BFF/OIDC" (9 patterns) + jose algo whitelist
- patterns/prisma.md : recette fusionnée "Migration String/Int → enum" (backfill + Cas A/B/C),
  row réactivable, endpoint replace atomique, updateMany conditionnel, etc.
- risques/general.md : 19 risques (epoch s vs ms, keepAliveTimeout=0, upsert+filtre liste,
  fail-safe catch-all, retrait asymétrique front/back, anti-énumération rate-limit, etc.)
- patterns/general, async, nestjs, contracts, tests + risques/auth, contracts, prisma, redis, stripe, tests
- compléments d'entrées existantes (authorize-after-fetch, P3014, cursor opaque, DI swc, Stripe v20...)
- README patterns/risques mis à jour

Doublons internes corrigés en relecture (suppression-champ .map() → general seul ;
e2e DB-based → tests.md seul). Doublons hors backend / entrées projet / rejets non intégrés.
Source 95_a_capitaliser.md non purgée à ce stade (purge en fin de capitalisation complète).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:25:02 +02:00

13 KiB


title: Backend — Risques & vigilance : Contracts domain: backend bucket: risques tags: [contracts, zod, validation, error-codes, requestid] applies_to: [analysis, implementation, review, debug] severity: high validated_on: 2026-04-07 source_projects: [app-alexandrie, RL799_V2]

Backend — Risques & vigilance : Contracts

Extrait de la base de connaissance Lead_tech. Voir knowledge/backend/risques/README.md pour l'index complet.


Contrats API implicites (validation faible ou absente)

Risques

  • Entrées non validées → erreurs bizarres / vulnérabilités
  • Changements qui cassent le front et les intégrations

Symptômes

  • 500 sur erreurs utilisateur
  • Incohérences de format de réponse
  • "Ça marche en staging, pas en prod" (données réelles)

Bonnes pratiques / mitigations

  • Schémas (OpenAPI/JSON Schema) + validation serveur
  • Formats de réponse cohérents
  • Versionner/éviter breaking changes

Erreurs non standardisées (4xx/5xx incohérents)

Risques

  • Front et automatisations impossibles à rendre robustes
  • Debug long (pas de codes internes, pas de corrélation)

Symptômes

  • Clients qui "retry" sur des 4xx
  • Messages techniques exposés aux utilisateurs
  • Logs inexploitables

Bonnes pratiques / mitigations

  • Mapping HTTP standard + format d'erreur stable
  • Codes internes d'erreurs applicatives
  • requestId/traceId partout

Duplication silencieuse de constantes partagées (contracts) via fichier orphelin

Risques

  • Deux sources de vérité qui divergent silencieusement (ex : topics officiels, enums métier, slugs)
  • Bug non détecté par TypeScript si la duplication est dans un fichier non importé (code mort)

Symptômes

  • Incohérences entre API et client sur des listes/enums "censées être partagées"
  • "Ça marche chez moi" selon l'endroit où la constante est importée
  • Un fichier de config existe dans apps/* mais n'est jamais importé/greffé au runtime

Bonnes pratiques / mitigations

  • Toute constante partagée vit dans packages/contracts/src/ et est importée depuis là (jamais recopiée dans apps/*)
  • En review : repérer les fichiers "config/constants" ajoutés dans apps/* sur des domaines déjà couverts par contracts
  • (Optionnel) Outillage : intégrer une étape de détection de code mort / exports inutilisés au CI si ça devient récurrent

Contracts : schema orphelin / type de retour désynchronisé

Risques

  • Un RequestSchema défini dans packages/contracts mais jamais importé dans le controller ni le service mobile → dead code silencieux qui crée une fausse confiance
  • Un type de retour inline (string brut) à la place du type contracts → désynchronisation silencieuse entre contrat et implémentation

Symptômes

  • grep du nom du schema ne trouve aucun import en dehors de sa définition
  • Service retourne Promise<{ status: string }> au lieu de Promise<CurationResponse> — le status n'est pas validé comme CurationStatus
  • Endpoints POST /action sans body ayant un schema { pathParam: string } — le param vient du path, pas du body

Bonnes pratiques / mitigations

À chaque story qui ajoute des schemas dans packages/contracts, vérifier en review :

  1. Chaque RequestSchema est utilisé dans un ZodValidationPipe (API) ou importé dans le service mobile.
  2. Les ResponseSchema correspondent au type de retour typé du service (Promise<TheType>, pas un type inline).
  3. Les endpoints sans body (POST /action) définissent z.object({}) ou omettent le body schema — ne jamais placer les path params dans le body schema.
// ❌ Anti-pattern — type inline, status non typé
async showcaseThread(...): Promise<{ threadId: string; status: string }> { ... }

// ✅ Pattern correct — type contracts importé
import type { CurationResponse } from '@app-alexandrie/contracts';
async showcaseThread(...): Promise<CurationResponse> { ... }
  • Contexte technique : NestJS / Zod / contracts-first — app-alexandrie 23-03-2026

Code d'erreur générique sur statut HTTP sémantique (409 CONFLICT)

Risques

  • Utiliser VALIDATION_ERROR ou INTERNAL_ERROR sur un 409 rend les erreurs indistinguables côté client et monitoring
  • Les clients (mobile, monitoring, tests) ne peuvent pas brancher une logique conditionnelle sans un code sémantique

Symptômes

  • Tous les conflits métier remontent le même code → impossible de distinguer "alias déjà résolu" de "handle déjà pris"
  • Tests forcés à matcher le message texte au lieu du code → fragiles

Bonnes pratiques / mitigations

Chaque scénario métier distinct doit avoir son propre code dans error-code.ts :

// ❌ Anti-pattern — code générique sur 409
throw new ConflictException({ error: { code: 'VALIDATION_ERROR', message: '...' } });

// ✅ Correct — code sémantique spécifique
throw new ConflictException({ error: { code: 'ALIAS_ALREADY_RESOLVED', message: '...' } });
throw new ConflictException({ error: { code: 'HANDLE_ALREADY_TAKEN', message: '...' } });
  • Règle : 1 scénario métier distinct = 1 code d'erreur distinct

  • Checklist review : tout 409/422 doit avoir un code dans error-code.ts, jamais VALIDATION_ERROR ou INTERNAL_ERROR

  • Contexte technique : NestJS / error-code.ts — app-alexandrie 24-03-2026


ForbiddenException (403) utilisé pour des erreurs de validation

Risques

  • Les clients qui filtrent par HTTP 400 manquent les erreurs de validation lancées en 403
  • Sémantique API incorrecte → comportements clients imprévisibles

Symptômes

  • ForbiddenException lancée pour des tags invalides, des formats incorrects, des liens HTTP
  • Clients API qui ignorent ces erreurs ou les traitent comme des refus d'accès

Bonnes pratiques / mitigations

Tableau de correspondance :

Cas Exception correcte Code HTTP
Tags invalides, contenu trop long, format incorrect BadRequestException 400
Accès refusé explicitement (accès forum, trial read-only) ForbiddenException 403
Quota dépassé HttpException(429) via HttpStatus.TOO_MANY_REQUESTS 429
  • Règle : HTTP 403 = "tu n'as pas le droit d'effectuer cette action". HTTP 400 = "ta requête est mal formée".
  • Contexte technique : NestJS / HTTP — 20-03-2026

Feature flags / config : lecture directe de process.env dans les services ou helpers métier

Risques

  • Tests verts malgré une dépendance implicite à l'état global du process
  • La validation Zod de l'env (ConfigService) existe mais est contournée au runtime via un helper non injecté
  • Story conforme "pas de process.env direct en service métier" mais violation dans un helper utilisé par le service

Symptômes

  • process.env.FEATURE_FLAG_X dans un helper métier plutôt que dans un module ConfigService
  • Tests passent mais comportement diverge selon l'env du process

Bonnes pratiques / mitigations

  • Ne jamais lire process.env directement dans les services ni les helpers métier.

  • Injecter ConfigService (NestJS) et centraliser la lecture via une fonction pure recevant la config injectée.

  • Checklist review : rechercher process.env dans src/ hors config/ ou main.ts — tout hit est suspect.

  • Contexte technique : NestJS / ConfigService — 30-03-2026


Nouveau statut métier non propagé dans le contrat partagé

Risques

  • Un repository ou service crée une nouvelle valeur pour un champ enum-like (ex: 'absent_auto' pour un champ status: String) sans la déclarer dans le type DTO ni le schema Zod du package shared
  • Le champ Prisma étant un String sans contrainte DB, l'écriture passe, mais toute lecture avec validation Zod rejetterait la donnée

Symptômes

  • String literal non présent dans le type union ou le schema Zod du DTO correspondant
  • catch {} vide dans les repositories qui avalent les erreurs de validation sans log

Bonnes pratiques / mitigations

Quand un repository ou service crée une nouvelle valeur pour un champ enum-like :

  1. Ajouter la valeur au type DTO dans packages/shared (ou équivalent)
  2. Ajouter la valeur au schema de validation Zod correspondant
  3. Vérifier que les endpoints de lecture qui parsent ces données acceptent la nouvelle valeur
  • Contexte technique : Zod / contrats partagés — RL799_V2 03-04-2026

Bug Zod 4 — z.string().email().toLowerCase().trim() rejette les emails à trim

Risques

  • Le pattern z.string().email().toLowerCase().trim() ne fait pas ce qu'il prétend en Zod 4 : .email() est une assertion qui valide le format brut, avant que les transforms .toLowerCase() / .trim() s'appliquent
  • Un email avec espace trailing ("BOB@X.FR ") est rejeté Invalid email au lieu d'être trim+lower

Symptômes

  • Test fixture BOB@X.FR (trailing space) → 400 alors que l'intention est bob@x.fr
  • Pattern présent dans plusieurs schémas du projet (visitorProfileLookupSchema, tenueVisitorCreateSchema, etc.)

Bonnes pratiques / mitigations

// ❌ Pattern legacy faux (Zod 4) — assertion AVANT transforms
const emailSchema = z.string().email().max(254).toLowerCase().trim();

// ✅ Pattern correct : trim/lower AVANT email assertion via pipe
const emailSchema = z
  .string()
  .trim()
  .toLowerCase()
  .pipe(z.string().email().max(254));

.pipe() chaîne deux schémas — le premier transforme (trim+lower), le second valide (email+max). L'ordre devient explicite et l'assertion est appliquée après normalisation.

Tests à ajouter : BOB@X.FR (trailing space) → bob@x.fr, ALICE@TEST.FR (leading + casse) → alice@test.fr. Si le schéma rejette Invalid email, le bug est présent.

À auditer projet-wide : grep tous les schémas avec ce pattern (.email().toLowerCase().trim()) et migrer en .pipe().

  • Contexte technique : Zod 4 — RL799_V2 01-05-2026

AC d'affichage livré « vert » mais champ absent du contract Zod

Risques

  • Un AC métier dit « afficher / truncate / preview de [champ] dans [carte/écran] » mais le champ n'est jamais ajouté au schéma Zod public correspondant → le user ne le voit jamais
  • Le service backend peut même charger le champ depuis la DB (select: { bio: true }) puis le jeter au mapping de réponse → invisible
  • Ni le typage ni les tests unit ne détectent l'absence : la code review est le seul filet

Symptômes

  • AC d'affichage livré « tout vert » (tests/typecheck/lint passent) mais l'écran ne montre rien
  • Variante : champ rendu côté UI mais jamais transmis par l'API → undefined/null silencieux à l'écran

Bonnes pratiques / mitigations

Quand un AC mentionne « afficher / truncate / preview de [champ] dans [carte/écran] », vérifier la chaîne complète :

  1. Le champ existe dans le schéma Zod public (ex : AC « carte annuaire affiche bio truncate 80 char » → DirectoryUserSchema doit avoir bio: z.string().nullable()).
  2. Le service backend l'expose dans le mapping de réponse (pas seulement dans le select Prisma).
  3. Le composant UI lit le champ.

Le contrat est la barrière minimale : si le champ n'y est pas, l'AC ne peut pas être satisfait.

  • Contexte technique : NestJS / Zod / contracts-first — app-alexandrie 28-05-2026

Schéma par audience, pas par entité : vue admin réutilise le schéma public

Risques

  • Une vue ADMIN/back-office réutilise (ou extend) le schéma de la vue PUBLIQUE/utilisateur de la même entité → elle hérite d'un schéma qui masque délibérément les champs de gestion
  • L'admin ne peut pas distinguer les états (ex : leçons DRAFT vs PUBLISHED) car le contrat ampute l'info structurante (status, body, timestamps, flags internes)
  • Le service inclut correctement les données (pas de filtre status) mais le CONTRAT les supprime — bug invisible au typecheck et au test

Symptômes

  • Un détail admin extend/réutilise un schéma existant qui est en réalité la variante de rendu front
  • Un test qui ne peut asserter que la présence d'un élément, jamais son état (le champ d'état n'existe pas dans le schéma)
  • AC « prévisualiser avant publication » / « back-office » non satisfait alors que tout est vert

Bonnes pratiques / mitigations

  • Une vue admin/back-office et une vue publique de la MÊME entité ne partagent PAS le schéma de réponse par défaut : l'admin a besoin des champs de gestion (status, body brut, timestamps, flags) que la vue publique masque.

  • Réflexe de revue : quand un détail admin réutilise un schéma, vérifier que c'est bien la variante ADMIN (porte le statut/les champs éditoriaux), pas la variante de rendu front.

  • Contexte technique : NestJS / Zod / contracts-first — app-alexandrie 04-06-2026