# 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

- [Format d’erreur 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 d’enregistrement 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)
- [RedisHealthService avec cache interne court](#pattern-redis-health-cache-court)
- [Sémantique explicite `Trial` vs `Paid` dans Subscription](#pattern-subscription-trial-vs-paid)
- [Restauration d’achats Stripe en 3 étapes](#pattern-restauration-achats-stripe)
- [Mapping explicite de `P2002` Prisma sur update de champ unique](#pattern-prisma-p2002-update-unique)

---

## Règle d’or

Si ce n’est pas confirmé comme fonctionnel et utile, **ça n’a rien à faire ici**.

- Pas de “bonnes pratiques” vagues
- Pas de dépendances implicites à une stack
- Si c’est 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 d’erreurs
- DB & migrations
- observabilité
- opérations sensibles (idempotence, retries)
- intégrations (webhooks, jobs async)

---

## Format standard d’un pattern

## Pattern : <Nom clair>

- Objectif : …
- Contexte : …
- Quand l’utiliser : …
- 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 d’entré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 d’erreur 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 l’utiliser : dès qu’une API est exposée à autre chose qu’un 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 d’erreur 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 l’utiliser : 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 à l’entrée si absent
- Le propager dans le contexte de requête
- L’inclure dans chaque log et réponse d’erreur
```

### Checklist

- requestId généré ou repris d’un 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 l’utiliser : 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 d’unicité 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 l’utiliser : dès qu’un listing peut dépasser quelques dizaines/centaines d’items ou subir des écritures concurrentes.
- Quand l’éviter : listes strictement petites et statiques.
- Avantage :
  - Résultats stables malgré insertions/suppressions
  - Meilleure performance que l’offset 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 d’emails, appels SaaS, génération de PDF, traitements batch, webhooks sortants.
- Quand l’utiliser : dès qu’une opération peut dépasser la latence acceptable ou dépendre d’un 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 l’idempotence
  - Nécessite une stratégie minimale de dead-letter ou d’alerting
- 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 d’audit, restauration ou conformité.
- Quand l’utiliser : dès qu’une 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 l’utiliser : dès qu’un é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 d’interface 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 l’utiliser : dès qu’une 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 d’erreur 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 d’erreur applicatifs stables
- Enums et constantes partagées (ex : liste officielle de sujets/topics)

### Ce qui n’appartient PAS à contracts

- Logique métier
- Modules/services/guards framework (NestJS, etc.)
- State management client (Zustand, Redux, etc.)

### Checklist

- [ ] Zéro DTO manuel dans l’API — uniquement `z.infer<typeof Schema>`
- [ ] `ZodValidationPipe` global ou par endpoint pour la validation d’entré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 d’enregistrement 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 l’utiliser : dès qu’on a 2+ guards globaux dont l’un dépend du résultat de l’autre.
- Quand l’éviter : si un seul guard suffit.
- Avantage :
  - Sécurité par défaut (opt-out, pas opt-in)
  - Ordre d’exécution garanti et explicite
  - Bypass documenté et traçable via décorateurs
- Limites / vigilance :
  - L’ordre des `APP_GUARD` dans `providers[]` est l’ordre d’exécution — ne pas inverser
  - Exporter le service depuis son module si injecté dans un guard global d’un 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 l’utiliser : dès qu’un service applicatif dépend d’un SDK tiers (et plus encore s’il 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 :
  - L’interface 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 l’utiliser : systématiquement dès qu’on 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 l’utiliser : dès qu’on 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 d’erreur 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 d’importer l’enum → 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 l’enum, pas une string littérale
- [ ] PR review : vérifier `error-code.ts` à chaque ajout d’endpoint d’erreur

---

<a id="pattern-redis-health-cache-court"></a>
## 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 l’utiliser : quand plusieurs requêtes concurrentes doivent consulter l’état Redis.
- Quand l’éviter : si Redis n’est 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)

```txt
- 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

---

<a id="pattern-subscription-trial-vs-paid"></a>
## 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 l’abonnement payant actif.
- Contexte : modèle `Subscription` où `trialEndsAt` matérialise un essai.
- Quand l’utiliser : dès qu’un 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 d’abonnement

### Implémentation (exemple minimal)

```txt
- Un abonnement payant actif n’est pas seulement status = ACTIVE
- Il doit aussi avoir trialEndsAt = null
- Les fixtures et mocks e2e d’un 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

---

<a id="pattern-restauration-achats-stripe"></a>
## Pattern : restauration d’achats Stripe en 3 étapes

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

### Implémentation (exemple minimal)

```txt
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

---

<a id="pattern-prisma-p2002-update-unique"></a>
## Pattern : mapping explicite de `P2002` Prisma sur update de champ unique

- Objectif : transformer un conflit d’unicité prévisible en erreur métier exploitable plutôt qu’en 500 opaque.
- Contexte : `update` Prisma sur un champ `@unique` alimenté par une source externe ou concurrente.
- Quand l’utiliser : dès qu’un 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 d’erreur API standardisé
- Validé le : 10-03-2026
- Contexte technique : Prisma / PostgreSQL / NestJS

### Implémentation (exemple minimal)

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

### Checklist

- `P2002` intercepté sur les updates sensibles
- Code d’erreur 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 d’utilisation.
