Files
_Assistant_Lead_Tech/10_backend_patterns_valides.md
MaksTinyWorkshop 5650f26b08 feat: capitalise Epic 2 app-alexandrie + enrichit post-bmad-install
- Intègre 9 propositions de 95_a_capitaliser.md (Stripe, webhooks, Redis,
  entitlements, guards, catch silencieux, conventions File List)
- Ajoute core-bmad-master dans les agents patchés (orchestrateur)
- Différencie les fichiers cibles par rôle d'agent (dev/architect/qa…)
- Patch dev-story et code-review XML pour déclencher la capitalisation
  à chaque fin de story et après chaque code review

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 14:13:34 +01:00

565 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Patterns back-end validés
Ce fichier contient **uniquement** des patterns back-end :
- testés,
- validés,
- utilisés en conditions réelles.
Objectif : éviter de réinventer la roue et réduire le temps de debug.
Dernière mise à jour : 09-03-2026
---
## Index
- [Format derreur API standardisé](#pattern-format-derreur-api-standardise)
- [Middleware de corrélation (requestId / traceId)](#pattern-middleware-correlation-requestid-traceid)
- [Idempotency key pour opérations sensibles](#pattern-idempotency-key-operations-sensibles)
- [Pagination robuste (cursor-based) pour les listings](#pattern-pagination-robuste-cursor-based)
- [Exécution asynchrone des tâches longues (queue + outbox light)](#pattern-execution-asynchrone-taches-longues)
- [Soft delete et archivage explicite](#pattern-soft-delete-archivage-explicite)
- [Webhooks sortants robustes et idempotents](#pattern-webhooks-sortants-robustes-idempotents)
- [Contracts-First / Zod-Infer / No-DTO (monorepo TypeScript fullstack)](#pattern-contracts-first-zod-infer-no-dto)
- [Guard global NestJS — ordre denregistrement et décorateurs de bypass](#pattern-guard-global-nestjs)
- [Provider-Strategy pour intégrations tierces — périmètre complet](#pattern-provider-strategy-integrations-tierces)
- [Stripe — metadata sur `subscription_data`, pas sur la Session](#pattern-stripe-subscription-metadata)
- [Webhooks entrants — parsing unique (single constructWebhookEvent)](#pattern-webhook-parsing-unique)
- [Contracts-First — error codes comme contrat obligatoire](#pattern-contracts-error-codes)
---
## Règle dor
Si ce nest pas confirmé comme fonctionnel et utile, **ça na rien à faire ici**.
- Pas de “bonnes pratiques” vagues
- Pas de dépendances implicites à une stack
- Si cest spécifique à un framework / runtime / DB : on le note
---
## Périmètre couvert
- API (REST/GraphQL), services applicatifs
- authn/authz
- contrats (validation / schémas)
- gestion derreurs
- DB & migrations
- observabilité
- opérations sensibles (idempotence, retries)
- intégrations (webhooks, jobs async)
---
## Format standard dun pattern
## Pattern : <Nom clair>
- Objectif : …
- Contexte : …
- Quand lutiliser : …
- Quand léviter : …
- Avantage : …
- Limites / vigilance : …
- Validé le : DD-MM-YYYY
- Contexte technique : (obligatoire) ex. `Node 20 / Postgres 16` ou `Python 3.12 / FastAPI / Redis`
### Implémentation (exemple minimal)
```txt
(contenu)
```
### Checklist (si pertinente)
- Erreurs standardisées
- Validation dentrée (schéma)
- Observabilité minimale (requestId/traceId + logs)
- Sécurité (authn/authz + secrets)
- Tests au bon niveau
- Idempotence si opération sensible
---
<a id="pattern-format-derreur-api-standardise"></a>
## Pattern : Format derreur API standardisé
- Objectif : fournir des erreurs prévisibles, exploitables et cohérentes pour tous les clients.
- Contexte : API consommée par front-end, automatisations ou intégrations externes.
- Quand lutiliser : dès quune API est exposée à autre chose quun usage interne trivial.
- Quand léviter : jamais.
- Avantage :
- Debug plus rapide
- UX maîtrisée côté client
- Observabilité améliorée
- Limites / vigilance :
- Discipline requise pour éviter les formats ad hoc
- Validé le : 25-01-2026
- Contexte technique : API HTTP agnostique
### Implémentation (exemple minimal)
```json
{
"error": {
"code": "USER_NOT_FOUND",
"message": "Utilisateur introuvable",
"requestId": "abc-123"
}
}
```
### Checklist
- Codes HTTP cohérents (4xx / 5xx)
- Codes derreur applicatifs stables
- Message utilisateur non technique
- requestId présent
---
<a id="pattern-middleware-correlation-requestid-traceid"></a>
## Pattern : Middleware de corrélation (requestId / traceId)
- Objectif : relier chaque requête aux logs et erreurs associées.
- Contexte : toute API ou service exposé.
- Quand lutiliser : systématiquement en production.
- Quand léviter : jamais.
- Avantage :
- MTTR réduit drastiquement
- Debug cross-services possible
- Limites / vigilance :
- Doit être propagé partout (logs, erreurs, appels sortants)
- Validé le : 25-01-2026
- Contexte technique : Backend agnostique (HTTP)
### Implémentation (exemple minimal)
```txt
- Générer un requestId à lentrée si absent
- Le propager dans le contexte de requête
- Linclure dans chaque log et réponse derreur
```
### Checklist
- requestId généré ou repris dun header existant
- Présent dans tous les logs
- Présent dans les erreurs retournées
---
<a id="pattern-idempotency-key-operations-sensibles"></a>
## Pattern : Idempotency key pour opérations sensibles
- Objectif : empêcher les doublons lors de retries ou timeouts.
- Contexte : création de ressources, paiements, webhooks.
- Quand lutiliser : toute opération non strictement en lecture.
- Quand léviter : endpoints purement GET.
- Avantage :
- Protection contre doublons
- Robustesse face aux retries
- Limites / vigilance :
- Stockage et expiration des clés à gérer
- Validé le : 25-01-2026
- Contexte technique : API HTTP + DB transactionnelle
### Implémentation (exemple minimal)
```txt
- Client fournit Idempotency-Key
- Backend stocke la clé + résultat
- Retry retourne le résultat initial
```
### Checklist
- Clé obligatoire sur endpoints sensibles
- Contrainte dunicité côté DB
- Comportement documenté
---
<a id="pattern-pagination-robuste-cursor-based"></a>
## Pattern : Pagination robuste (cursor-based) pour les listings
- Objectif : fournir des listings stables et performants sans incohérences entre pages.
- Contexte : endpoints de liste (ex. /users, /orders) avec volume potentiellement important.
- Quand lutiliser : dès quun listing peut dépasser quelques dizaines/centaines ditems ou subir des écritures concurrentes.
- Quand léviter : listes strictement petites et statiques.
- Avantage :
- Résultats stables malgré insertions/suppressions
- Meilleure performance que loffset sur gros volumes
- Expérience client plus fiable
- Limites / vigilance :
- Nécessite un tri déterministe (champ + tie-breaker)
- Complexité légèrement supérieure à offset/limit
- Validé le : 25-01-2026
- Contexte technique : API HTTP + DB (Postgres/MySQL), agnostique framework
### Implémentation (exemple minimal)
```txt
- Trier par (createdAt DESC, id DESC) (exemple)
- Le client envoie cursor = dernier (createdAt,id) reçu
- Le backend renvoie nextCursor si plus de résultats
- Ne jamais exposer de cursor implicite ou non documenté
```
### Checklist
- Tri déterministe (avec tie-breaker)
- nextCursor renvoyé et documenté
- Limite max de page (protection)
- Index DB aligné avec le tri
---
<a id="pattern-execution-asynchrone-taches-longues"></a>
## Pattern : Exécution asynchrone des tâches longues (queue + outbox light)
- Objectif : sortir les opérations longues ou fragiles du chemin request/response.
- Contexte : envoi demails, appels SaaS, génération de PDF, traitements batch, webhooks sortants.
- Quand lutiliser : dès quune opération peut dépasser la latence acceptable ou dépendre dun service externe.
- Quand léviter : opérations réellement instantanées et sans dépendances externes.
- Avantage :
- API plus rapide et plus fiable
- Retries maîtrisés
- Meilleure résilience aux pannes externes
- Limites / vigilance :
- Demande une discipline stricte sur lidempotence
- Nécessite une stratégie minimale de dead-letter ou dalerting
- Validé le : 25-01-2026
- Contexte technique : Backend agnostique + DB transactionnelle + worker
### Implémentation (exemple minimal)
```txt
- API écrit un job ou event en DB dans la transaction métier
- Worker lit les jobs en attente et exécute
- Retries avec backoff + compteur
- Statut FAILED ou dead-letter + alerte
- Idempotence par clé métier ou idempotency key
```
### Checklist
- Job créé dans une transaction (évite les pertes)
- Retries et backoff définis
- Dead-letter ou statut FAILED visible
- Idempotence garantie
- Logs corrélés (requestId/traceId)
---
<a id="pattern-soft-delete-archivage-explicite"></a>
## Pattern : Soft delete et archivage explicite
- Objectif : permettre la suppression logique sans perte immédiate de données.
- Contexte : données métier critiques, besoins daudit, restauration ou conformité.
- Quand lutiliser : dès quune suppression peut avoir des impacts métier ou légaux.
- Quand léviter : données purement techniques ou réellement éphémères.
- Avantage :
- Restauration possible
- Audit et traçabilité
- Réduction des suppressions irréversibles
- Limites / vigilance :
- Complexité accrue sur les requêtes
- Nécessite une discipline stricte (filtres par défaut)
- Validé le : 25-01-2026
- Contexte technique : API + DB relationnelle
### Implémentation (exemple minimal)
```txt
- Champ deletedAt (nullable) ou status
- Les requêtes standards filtrent deletedAt IS NULL
- Endpoints dédiés pour restauration / purge
- Index DB tenant compte du soft delete
```
### Checklist
- Filtrage soft delete par défaut
- Restauration explicite possible
- Purge maîtrisée (cron / job)
- Index DB adaptés
- Tests sur cas supprimé / restauré
---
<a id="pattern-webhooks-sortants-robustes-idempotents"></a>
## Pattern : Webhooks sortants robustes et idempotents
- Objectif : garantir des intégrations fiables avec des systèmes externes.
- Contexte : notifications, synchronisations, événements métier sortants.
- Quand lutiliser : dès quun événement doit être transmis à un tiers.
- Quand léviter : intégrations strictement synchrones et internes.
- Avantage :
- Tolérance aux pannes réseau
- Retries maîtrisés
- Observabilité des échecs
- Limites / vigilance :
- Gestion des retries et du volume
- Nécessite une idempotence côté consommateur
- Validé le : 25-01-2026
- Contexte technique : Backend + HTTP + worker/queue
### Implémentation (exemple minimal)
```txt
- Événement persisté (outbox) en DB
- Envoi asynchrone via worker
- Retries avec backoff
- Signature du payload (HMAC)
- Idempotency key dans le header
```
### Checklist
- Payload signé et vérifiable
- Retries + backoff définis
- Dead-letter ou statut FAILED visible
- Idempotence documentée
- Logs corrélés (requestId/traceId)
---
<a id="pattern-contracts-first-zod-infer-no-dto"></a>
## Pattern : Contracts-First / Zod-Infer / No-DTO (monorepo TypeScript fullstack)
- Objectif : avoir une seule source de vérité pour les contrats dinterface entre API et client, sans redéfinition manuelle de types.
- Contexte : monorepo TypeScript avec un package partagé (`packages/contracts` ou équivalent), consommé par le backend et le front/mobile.
- Quand lutiliser : dès quune API est consommée par un client TypeScript dans le même repo.
- Quand léviter : si le client est externe (autre organisation, autre langage) — dans ce cas, OpenAPI reste la référence.
- Avantage :
- Zéro drift entre contrat et implémentation
- Types TypeScript gratuits via `z.infer<>` — aucune réécriture
- Changement de contrat = erreur de compilation immédiate côté client
- Mocks de tests alignés automatiquement
- Limites / vigilance :
- Ne pas mettre de logique métier dans `packages/contracts` (IO only)
- Attention aux dépendances circulaires si le package grossit
- Validé le : 07-03-2026
- Contexte technique : TypeScript / Zod / NestJS + Expo (React Native) — pattern agnostique framework
### Implémentation (exemple minimal)
```typescript
// packages/contracts/src/auth/auth.schemas.ts
export const RegisterRequestSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export type RegisterRequest = z.infer<typeof RegisterRequestSchema>; // type GRATUIT
// packages/contracts/src/index.ts
export * from ./auth/auth.schemas;
export * from ./errors/error-code;
// apps/api/src/modules/auth/auth.controller.ts
import type { RegisterRequest } from @monrepo/contracts;
// + ZodValidationPipe → validation automatique, zéro DTO manuel
// apps/mobile/src/domains/auth/auth.store.ts
import type { RegisterRequest } from @monrepo/contracts;
// même type, même schéma, zéro duplication
```
### Structure cible du package contracts
```
packages/contracts/src/
auth/auth.schemas.ts ← request/response auth
users/users.schemas.ts ← request/response users
billing/billing.schemas.ts ← request/response billing (Epic suivant)
errors/error-code.ts ← enum codes derreur stables
http/envelopes.ts ← { data, meta } / { error, meta }
index.ts ← re-export tout
```
### Ce qui appartient à contracts
- Schémas Zod request/response
- Types inférés (`z.infer<>`)
- Codes derreur applicatifs stables
- Enums et constantes partagées (ex : liste officielle de sujets/topics)
### Ce qui nappartient PAS à contracts
- Logique métier
- Modules/services/guards framework (NestJS, etc.)
- State management client (Zustand, Redux, etc.)
### Checklist
- [ ] Zéro DTO manuel dans lAPI — uniquement `z.infer<typeof Schema>`
- [ ] `ZodValidationPipe` global ou par endpoint pour la validation dentrée
- [ ] Constantes partagées (enums, listes) dans contracts, jamais dupliquées
- [ ] Mocks de tests importent les types depuis contracts
---
<a id="pattern-guard-global-nestjs"></a>
## Pattern : Guard global NestJS — ordre denregistrement et décorateurs de bypass
- Objectif : protéger tous les endpoints par défaut, avec un mécanisme explicite pour les exceptions.
- Contexte : API NestJS avec plusieurs guards globaux (authn, authz, feature flags...).
- Quand lutiliser : dès quon a 2+ guards globaux dont lun dépend du résultat de lautre.
- Quand léviter : si un seul guard suffit.
- Avantage :
- Sécurité par défaut (opt-out, pas opt-in)
- Ordre dexécution garanti et explicite
- Bypass documenté et traçable via décorateurs
- Limites / vigilance :
- Lordre des `APP_GUARD` dans `providers[]` est lordre dexécution — ne pas inverser
- Exporter le service depuis son module si injecté dans un guard global dun autre module
- Validé le : 07-03-2026
- Contexte technique : NestJS v10+
### Implémentation (exemple minimal)
```typescript
// app.module.ts
providers: [
{ provide: APP_GUARD, useClass: AuthGuard }, // 1er : peuple request.user
{ provide: APP_GUARD, useClass: EmailVerifiedGuard }, // 2ème : lit request.user
{ provide: APP_GUARD, useClass: EntitlementsGuard }, // 3ème : lit request.user + entitlements
]
// skip-auth.decorator.ts
export const SKIP_AUTH = skipAuth;
export const SkipAuth = () => SetMetadata(SKIP_AUTH, true);
// auth.guard.ts
const skip = this.reflector.getAllAndOverride<boolean>(SKIP_AUTH, [
context.getHandler(),
context.getClass(), // permet @SkipAuth() au niveau classe
]);
if (skip) return true;
```
### Checklist
- [ ] AuthGuard enregistré en premier dans `providers[]`
- [ ] AuthModule exporte AuthService si AuthGuard est dans AppModule
- [ ] Décorateur `@SkipAuth()` sur tous les endpoints publics (auth, health, docs)
- [ ] Tests unitaires sur le guard avec reflector mocké
---
<a id="pattern-provider-strategy-integrations-tierces"></a>
## Pattern : Provider-Strategy pour intégrations tierces — périmètre complet
- Objectif : isoler intégralement la logique propre à un prestataire (Stripe, Brevo, Firebase…) derrière une interface stable, pour éviter la contamination du domaine par le SDK tiers.
- Contexte : backend NestJS/TypeScript avec 1+ prestataires externes (paiement, email, storage…).
- Quand lutiliser : dès quun service applicatif dépend dun SDK tiers (et plus encore sil y a des webhooks).
- Quand léviter : intégration ponctuelle non critique sans effet de bord (rare) — sinon on perd vite le contrôle.
- Avantage :
- Testabilité : mock du provider, pas du SDK
- Remplacement du prestataire sans refactor “en cascade”
- Responsabilités claires : provider = “parle Stripe”, service = “parle domaine”
- Limites / vigilance :
- Linterface doit exposer des **types normalisés** (pas de types Stripe)
- Le provider gère aussi les webhooks : validation signature, parsing event, mapping
- Validé le : 09-03-2026
- Contexte technique : NestJS v10+ / intégration Stripe (webhooks) — pattern généralisable
### Implémentation (exemple minimal)
```typescript
// billing-provider.interface.ts (pas d'import Stripe)
export type BillingPlan = 'MONTHLY' | 'ANNUAL';
export type BillingWebhookResult = {
userId: string;
externalId: string;
plan: BillingPlan;
status: 'ACTIVE' | 'INACTIVE' | 'CANCELLED';
currentPeriodEnd: Date | null;
};
export interface BillingProvider {
createCheckoutSession(userId: string, plan: BillingPlan): Promise<{ checkoutUrl: string }>;
cancelSubscription(externalId: string): Promise<void>;
handleWebhook(rawBody: Buffer, signature: string): Promise<BillingWebhookResult | null>;
}
// billing.service.ts (domaine uniquement)
async handleWebhook(rawBody: Buffer, signature: string): Promise<void> {
const result = await this.billingProvider.handleWebhook(rawBody, signature);
if (!result) return;
await this.prisma.subscription.upsert({ /* données normalisées */ });
}
```
---
<a id=”pattern-stripe-subscription-metadata”></a>
## Pattern : Stripe — metadata sur `subscription_data`, pas sur la Session
- Objectif : garantir que `userId` (ou tout identifiant métier) soit accessible dans les events `customer.subscription.*`, pas seulement dans `checkout.session.completed`.
- Contexte : intégration Stripe Checkout avec webhooks abonnement.
- Quand lutiliser : systématiquement dès quon crée une Checkout Session liée à une Subscription.
- Risque si ignoré : `metadata.userId` absent des events `customer.subscription.updated/deleted` → silent failure en prod.
- Validé le : 09-03-2026
- Contexte technique : Stripe API v17+ / NestJS
### Implémentation
```typescript
stripe.checkout.sessions.create({
metadata: { userId }, // pour checkout.session.completed
subscription_data: { metadata: { userId } }, // pour customer.subscription.*
});
```
---
<a id=”pattern-webhook-parsing-unique”></a>
## Pattern : Webhooks entrants — parsing unique (single `constructWebhookEvent`)
- Objectif : appeler `constructWebhookEvent` une seule fois par requête, puis router vers des extracteurs purs.
- Contexte : endpoint webhook recevant des events de plusieurs types (subscription, pack, facture…).
- Quand lutiliser : dès quon a 2+ handlers webhook sur le même endpoint.
- Risque si ignoré : double vérification de signature + états partiels possibles (sub OK / pack KO).
- Validé le : 09-03-2026
- Contexte technique : Stripe / NestJS
### Implémentation
```typescript
// 1. Parser unique — 1 seul constructWebhookEvent(rawBody, sig) → event opaque
// 2. Extracteurs purs, sans effet de bord :
handleSubscriptionWebhookEvent(event): WebhookResult | null
handlePackWebhookEvent(event): PackWebhookResult | null
// 3. Orchestrateur unique appelle les extracteurs, persiste les résultats
```
---
<a id=”pattern-contracts-error-codes”></a>
## Pattern : Contracts-First — error codes comme contrat obligatoire
- Objectif : maintenir les codes derreur API dans `packages/contracts` pour éviter les clients stringly-typed.
- Contexte : monorepo TypeScript avec `packages/contracts/src/errors/error-code.ts`.
- Règle : toute nouvelle erreur API ⇒ ajout obligatoire dans `error-code.ts` **avant merge**, pas après.
- Risque si ignoré : clients qui testent des strings hardcodées au lieu dimporter lenum → drift silencieux.
- Validé le : 09-03-2026
- Contexte technique : TypeScript / NestJS + Expo (React Native)
### Checklist
- [ ] Nouvel `error.code` → ajout dans `packages/contracts/src/errors/error-code.ts` en même commit
- [ ] Clients importent lenum, pas une string littérale
- [ ] PR review : vérifier `error-code.ts` à chaque ajout dendpoint derreur
---
### Notes importantes
- On préfère 5 patterns solides à 50 “bons conseils”.
- Un pattern = une idée actionnable + son cadre dutilisation.