# Backend — Patterns : Prisma > Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet. --- ## 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é --- ## 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 --- ## 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é --- ## Pattern : mapping explicite de `P2002` Prisma sur create/update de champ unique - Objectif : transformer un conflit d'unicité prévisible en erreur métier exploitable plutôt qu'en 500 opaque. - Contexte : `create`, `update` ou `upsert` Prisma sur un champ `@unique` alimenté par une source externe, concurrente, ou après un pre-check. - Quand l'utiliser : dès qu'un champ unique peut entrer en collision — à la création ET à la modification. - 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é ``` ### Implémentation (exemple complet) ```typescript import { Prisma } from "@prisma/client"; try { await prisma.item.create({ data: { ... } }); // ou: await prisma.item.update({ where: { id }, data: { ... } }); } catch (err) { if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") { throw new HttpError("Un élément avec ce nom existe déjà.", { status: 409 }); } throw err; } ``` **Important :** un pre-check applicatif (`findUnique` avant `create`) ne suffit pas contre les race conditions. Le `try/catch P2002` est le seul garde-fou fiable. S'applique à `create`, `update`, `updateMany`, `upsert`. ### Checklist - `P2002` intercepté sur les creates ET les updates sensibles - Code d'erreur métier stable (409 Conflict) - Pas de 500 générique sur conflit prévisible --- ## Pattern : Sérialiser les champs `Decimal` Prisma en string au niveau du repository - Objectif : éviter que les objets `Decimal` Prisma traversent les couches et causent des erreurs de sérialisation JSON silencieuses. - Contexte : tout champ `Decimal` en Prisma (ex: `price`) retourné via API ou Server Action. - Quand l'utiliser : systématiquement sur tout champ `Decimal` dans les repositories. - Risque si ignoré : `Decimal` n'est pas JSON-sérialisable nativement — comportement varie selon Node vs browser vs test runner. - Validé le : 17-03-2026 - Contexte technique : Prisma / Node.js — app-template-resto ### Implémentation ```typescript // Repository — convertir avant de retourner return { ...dish, price: dish.price?.toString() ?? null, // Decimal → string }; // DTO public type DishDto = { price: string | null; // pas Decimal }; ``` --- ## Pattern : Prisma — Migration manuelle sans shadow DB (P3014) - Objectif : créer et appliquer une migration Prisma quand la shadow database est interdite (DB managée, permissions restreintes). - Contexte : DB managées — Supabase, PlanetScale, Railway avec rôle limité, RDS sans superuser. - Quand l'utiliser : quand `prisma migrate dev` échoue avec `P3014 Prisma Migrate could not create the shadow database`. - Risque si ignoré : blocage complet de la migration sur env managé. - Validé le : 23-03-2026 - Contexte technique : Prisma v7+ — app-alexandrie / Supabase ### Implémentation ```bash # 1. Écrire le SQL manuellement mkdir -p prisma/migrations/_ # Créer migration.sql à la main # 2. Appliquer le SQL directement en DB npx prisma db execute --file prisma/migrations/_/migration.sql # 3. Marquer la migration comme appliquée dans _prisma_migrations npx prisma migrate resolve --applied _ # Note Prisma v7 : ne pas utiliser --schema= (option supprimée), utiliser prisma.config.ts ``` **Ne pas utiliser `prisma db push` en production** — il ne versionne pas les migrations. --- ## Pattern : Filtrage des règles métier dans le service, pas dans le repository - Objectif : séparer la couche d'accès aux données (repository) des règles de visibilité métier (service). - Contexte : entités publiques avec règles de filtrage (`isVisible`, `isActive`), qui varient selon le contexte appelant (public vs admin). - Quand l'utiliser : dès qu'une règle de visibilité dépend du contexte d'appel. - Quand l'éviter : filtres de performance (pagination, tenant scoping) — ceux-là restent dans le `where`. - Avantage : - la règle est testable unitairement sans Prisma (mock de données brutes) - la requête DB reste simple et stable entre contextes - les cas futurs (ex: admin voit les invisibles) ne nécessitent pas de modifier la requête - Validé le : 17-03-2026 - Contexte technique : Prisma / Node.js / Next.js — app-template-resto ### Implémentation (exemple minimal) ```typescript // Repository — charge tout ce qui est candidat async findCategories(tenantId: string) { return prisma.category.findMany({ where: { tenantId } }); // pas de filtre isVisible } // Service — applique la règle métier et mappe vers DTO const raw = await repo.findCategories(tenantId); return raw.filter(c => c.isVisible).map(toPublicDto); // Admin : même repo, filtre différent dans le service admin return raw.map(toAdminDto); // retourne tout, visible ou non ```