Files
_Assistant_Lead_Tech/10_backend_patterns_valides.md

25 KiB
Raw Blame History

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 : 10-03-2026


Index


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 :

  • 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)

(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

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)

{
  "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

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)

- 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

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)

- 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é

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)

- 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

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)

- 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)

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)

- 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é

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)

- É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)

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)

// 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

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)

// 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é

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)

// 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 */ });
}

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

stripe.checkout.sessions.create({
  metadata: { userId },                           // pour checkout.session.completed
  subscription_data: { metadata: { userId } },    // pour customer.subscription.*
});

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

// 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

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

Pattern : RedisHealthService avec cache interne court

  • Objectif : exposer un état Redis exploitable par les guards globaux sans ping Redis à chaque requête.
  • Contexte : backend Node/NestJS avec Redis consulté dans le chemin de décision décriture.
  • Quand lutiliser : quand plusieurs requêtes concurrentes doivent consulter létat Redis.
  • Quand léviter : si Redis nest pas consulté dans le chemin request/response.
  • Avantage :
    • réduit fortement le flood de PING
    • garde un signal détat suffisamment frais
  • Limites / vigilance :
    • la fenêtre de cache doit rester courte
    • létat initial doit être explicite et assumé
  • Validé le : 10-03-2026
  • Contexte technique : NestJS / Redis

Implémentation (exemple minimal)

- Mémoriser lastStatus et lastCheck
- Si le dernier check a moins de 5s, retourner létat en cache
- Sinon exécuter un vrai PING et mettre le cache à jour
- Utiliser un état initial optimiste (`up`) si le produit ne doit pas bloquer les écritures au boot

Checklist

  • Cache court documenté
  • Pas de ping Redis à chaque requête
  • Comportement initial explicite

Pattern : Sémantique explicite Trial vs Paid dans Subscription

  • Objectif : aligner le modèle métier, les guards et les jeux de tests sur une définition unique de labonnement payant actif.
  • Contexte : modèle SubscriptiontrialEndsAt matérialise un essai.
  • Quand lutiliser : dès quun même enregistrement supporte trial et abonnement payant.
  • Quand léviter : si trial et abonnement payant sont modélisés par des entités distinctes.
  • Avantage :
    • évite les incohérences silencieuses dans les guards
    • rend les fixtures et mocks e2e cohérents avec la règle métier
  • Limites / vigilance :
    • toute logique isActive doit préciser si elle signifie “trial ou paid” ou “paid only”
  • Validé le : 10-03-2026
  • Contexte technique : Backend agnostique / modèle dabonnement

Implémentation (exemple minimal)

- Un abonnement payant actif nest pas seulement status = ACTIVE
- Il doit aussi avoir trialEndsAt = null
- Les fixtures et mocks e2e dun abonnement payant fixent toujours trialEndsAt: null

Checklist

  • Règle métier explicitée
  • Guards alignés sur la sémantique choisie
  • Fixtures et seeds cohérents

Pattern : restauration dachats Stripe en 3 étapes

  • Objectif : reconstruire un état local cohérent à partir de Stripe sans dépendre dune hypothèse fragile.
  • Contexte : flux de restore purchases mobile/web avec état local potentiellement désynchronisé.
  • Quand lutiliser : dès quun utilisateur peut restaurer des achats depuis un nouveau device ou après désynchronisation.
  • Quand léviter : si létat Stripe nest pas la source de vérité.
  • Avantage :
    • rend la réconciliation explicite
    • supporte retries et restaurations tardives
  • Limites / vigilance :
    • la pagination Stripe et lidempotence décriture restent obligatoires
  • Validé le : 10-03-2026
  • Contexte technique : Stripe API / backend Node/NestJS

Implémentation (exemple minimal)

1. Résolution du customer Stripe (ID persisté en DB, fallback robuste si absent)
2. Reconstruction de létat Stripe utile au domaine
3. Réconciliation et écritures locales idempotentes

Checklist

  • stripeCustomerId persistant côté app
  • Réconciliation explicite documentée
  • Upsert ou écriture idempotente

Pattern : mapping explicite de P2002 Prisma sur update de champ unique

  • Objectif : transformer un conflit dunicité prévisible en erreur métier exploitable plutôt quen 500 opaque.
  • Contexte : update Prisma sur un champ @unique alimenté par une source externe ou concurrente.
  • Quand lutiliser : dès quun champ unique peut être mis à jour après création.
  • Quand léviter : jamais si le champ peut réellement entrer en collision.
  • Avantage :
    • réponse client stable
    • diagnostic métier plus rapide
  • Limites / vigilance :
    • le mapping doit rester cohérent avec le format derreur API standardisé
  • Validé le : 10-03-2026
  • Contexte technique : Prisma / PostgreSQL / NestJS

Implémentation (exemple minimal)

- Catch explicite de PrismaClientKnownRequestError code P2002
- Mapping vers une erreur métier stable
- Conserver requestId et format derreur standardisé

Checklist

  • P2002 intercepté sur les updates sensibles
  • Code derreur métier stable
  • Pas de 500 générique sur conflit prévisible

Notes importantes

  • On préfère 5 patterns solides à 50 “bons conseils”.
  • Un pattern = une idée actionnable + son cadre dutilisation.