mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 13:31:43 +02:00
Intègre ~50 entrées depuis 95_a_capitaliser.md vers les fichiers validés :
- backend risques : +15 (GET sans authz, TOCTOU tenantId, TTL UTC, AdminRoleGuard, P3014...)
- backend patterns : P2002 amendé (create+update) + 10 nouveaux (Decimal, URL safe, EN enforcement...)
- frontend risques : +21 (defaultValue/key, useTransition global, consent state, Tailwind invalide...)
- frontend patterns : +6 (click-to-load, toggle optimiste, Server Action retourne entité...)
- debug/postmortem : export{fn} ne crée pas de binding local
95_a_capitaliser.md remis à l'état initial vide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
57 KiB
57 KiB
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 : 23-03-2026
Index
- Format d’erreur API standardisé
- Middleware de corrélation (requestId / traceId)
- Idempotency key pour opérations sensibles
- Pagination robuste (cursor-based) pour les listings
- Exécution asynchrone des tâches longues (queue + outbox light)
- Soft delete et archivage explicite
- Webhooks sortants robustes et idempotents
- Contracts-First / Zod-Infer / No-DTO (monorepo TypeScript fullstack)
- Guard global NestJS — ordre d’enregistrement et décorateurs de bypass
- Provider-Strategy pour intégrations tierces — périmètre complet
- Stripe — metadata sur
subscription_data, pas sur la Session - Webhooks entrants — parsing unique (single constructWebhookEvent)
- Contracts-First — error codes comme contrat obligatoire
- RedisHealthService avec cache interne court
- Sémantique explicite
TrialvsPaiddans Subscription - Restauration d’achats Stripe en 3 étapes
- Mapping explicite de
P2002Prisma sur create/update de champ unique - Autorisation interne minimale sans RBAC complet
- Anti-énumération sur endpoints auth liés à un email
- Token à usage unique — génération, hash et invalidation atomique
- Next.js runtime-only — orchestration en bord et logique pure testable
- Guardrails multi-tenant — 403 vs 404 selon la sémantique
- Repository tenant-aware —
tenantIdobligatoire dans la signature - Défense en profondeur — inclure
tenantIddans les updates - Next.js server-only & Server Actions — règles d'isolation
- Opérations auth sensibles — atomiques, idempotentes et cohérentes
- Réponse HTTP 200 avec payload métier pour les états d'accès
- Quota journalier Redis atomique (INCR + EXPIREAT pipeline)
- Filtrage des règles métier dans le service, pas dans le repository
- Sérialiser les champs
DecimalPrisma en string au niveau du repository - Extraire les helpers de résolution tenant dans un module partagé dédié
- Helper centralisé d'activation de features tenant-scoped
- Réutiliser un champ existant plutôt que créer un modèle dédié en V1
- Valider le protocole d'une URL externe avant de la passer à un lien public
- Utilitaires purs : extraire dans un module sans
server-only - EN enforcement optionnel par tenant (toggle + publish gate)
- Prisma — Migration manuelle sans shadow DB (P3014)
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 :
- Objectif : …
- Contexte : …
- Quand l’utiliser : …
- Quand l’éviter : …
- Avantage : …
- Limites / vigilance : …
- Validé le : DD-MM-YYYY
- Contexte technique : (obligatoire) ex.
Node 20 / Postgres 16ouPython 3.12 / FastAPI / Redis
Implémentation (exemple minimal)
(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
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)
{
"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
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)
- 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
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)
- 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 : 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)
- 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 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)
- 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 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)
- 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 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)
- É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 d’interface entre API et client, sans redéfinition manuelle de types.
- Contexte : monorepo TypeScript avec un package partagé (
packages/contractsou é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
- Ne pas mettre de logique métier dans
- 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 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> ZodValidationPipeglobal 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
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_GUARDdansproviders[]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
- L’ordre des
- 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 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)
// 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 eventscustomer.subscription.*, pas seulement danscheckout.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.userIdabsent des eventscustomer.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
constructWebhookEventune 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
// 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 d’erreur API dans
packages/contractspour é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.tsavant 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 danspackages/contracts/src/errors/error-code.tsen 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
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
- réduit fortement le flood de
- 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 l’abonnement payant actif.
- Contexte : modèle
SubscriptionoùtrialEndsAtmaté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
isActivedoit préciser si elle signifie “trial ou paid” ou “paid only”
- toute logique
- Validé le : 10-03-2026
- Contexte technique : Backend agnostique / modèle d’abonnement
Implémentation (exemple minimal)
- 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
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)
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
stripeCustomerIdpersistant côté app- Réconciliation explicite documentée
- Upsert ou écriture idempotente
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,updateouupsertPrisma sur un champ@uniquealimenté 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)
- Catch explicite de PrismaClientKnownRequestError code P2002
- Mapping vers une erreur métier stable
- Conserver requestId et format d’erreur standardisé
Implémentation (exemple complet)
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
P2002intercepté 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 : Autorisation interne minimale sans RBAC complet
- Objectif : sécuriser une capacité interne sensible sans ouvrir trop tôt un chantier RBAC complet.
- Contexte : application avec peu de rôles, besoin ponctuel d’une capacité admin ou opérateur clairement identifiée.
- Quand l’utiliser : quand une story métier demande un pouvoir interne limité mais réel.
- Quand l’éviter : si les permissions deviennent nombreuses, hiérarchiques ou contextuelles.
- Avantage :
- sécurisation rapide et lisible d’une capacité sensible
- source de vérité backend explicite
- chemin d’évolution propre vers un RBAC plus complet
- Limites / vigilance :
- ne pas laisser proliférer des rôles ad hoc non gouvernés
- ne remplace pas un vrai modèle de permissions si le domaine grossit
- Validé le : 10-03-2026
- Contexte technique : NestJS / auth par session ou JWT / API métier interne
Implémentation (exemple minimal)
- introduire un enum de rôle minimal côté backend (ex. USER | ADMIN)
- propager ce rôle dans la session ou le token d’auth
- créer un décorateur + guard dédiés pour la capacité sensible
- interdire les booléens front, emails hardcodés ou `if` dispersés dans les contrôleurs
Checklist
- Le rôle vit dans la source de vérité backend
- Le rôle est propagé dans le mécanisme d’auth existant
- Les endpoints sensibles passent par un guard dédié
- Aucun contrôle d’accès critique n’est piloté par le front
- Le passage à RBAC reste possible sans casser le contrat existant
Notes importantes
- On préfère 5 patterns solides à 50 “bons conseils”.
- Un pattern = une idée actionnable + son cadre d’utilisation.
Pattern : Anti-énumération sur endpoints auth liés à un email
- Objectif : empêcher qu’un endpoint auth révèle si un compte existe, n’existe pas ou n’est pas éligible.
- Contexte : reset de mot de passe, invitation, vérification de compte, login ou tout flux qui part d’un email utilisateur.
- Quand l’utiliser : dès qu’une requête auth touche un identifiant de type email.
- Quand l’éviter : jamais sur une surface exposée.
- Avantage :
- réduit la fuite d’information sur les comptes existants
- homogénéise les réponses côté client
- se combine bien avec les garde-fous anti-abus
- Limites / vigilance :
- ne protège pas seul contre le brute-force, à combiner avec du rate-limiting
- les logs internes doivent conserver la vraie cause sans l’exposer au client
- Validé le : 16-03-2026
- Contexte technique : Node.js / auth applicative / API HTTP
Implémentation (exemple minimal)
- retourner la même réponse HTTP 200 qu’un compte existe ou non
- ne jamais distinguer "email inconnu", "email connu" ou "compte OAuth-only" dans la réponse
- journaliser la cause réelle côté serveur
- ajouter un rate-limiting basé sur email + IP
Checklist
- Réponse client uniforme pour les cas compte connu/inconnu/non éligible
- Aucune fuite d’existence dans le message ou le code d’erreur
- Rate-limiting présent sur les endpoints exposés
- Logs internes exploitables
Pattern : Token à usage unique — génération, hash et invalidation atomique
- Objectif : standardiser la création et la consommation de tokens sensibles sans stocker de secret brut en base.
- Contexte : invitation, reset de mot de passe, vérification d’email, lien magique ou tout token one-shot.
- Quand l’utiliser : pour tout token à usage unique transmis à l’utilisateur.
- Quand l’éviter : sessions longues ou secrets devant être relus en clair côté serveur.
- Avantage :
- réduit l’impact d’une fuite de base
- garde des tokens URL-safe
- favorise une consommation atomique et réutilisable
- Limites / vigilance :
- la consommation doit rester atomique
- la politique d’expiration doit être explicite
- Validé le : 16-03-2026
- Contexte technique : Node.js
crypto/ Prisma / email ou URL signée
Implémentation (exemple minimal)
- générer le token avec `crypto.randomBytes(32).toString("base64url")`
- stocker uniquement le hash SHA-256 du token en base
- transmettre le token brut uniquement via URL ou email
- recalculer le hash côté serveur lors de la consommation
- invalider le token dans une transaction atomique après usage
Checklist
- Token brut jamais persisté en base
- Hash recalculé côté serveur pour la vérification
- Expiration explicite
- Invalidation atomique après consommation
Pattern : Next.js runtime-only — orchestration en bord et logique pure testable
- Objectif : préserver la testabilité unitaire et la lisibilité du code serveur Next.js en limitant les dépendances runtime-only aux couches d’orchestration.
- Contexte : applications Next.js avec Server Actions, route handlers, modules email/auth et logique métier testée côté Node.
- Quand l’utiliser : dès qu’un flux serveur mélange APIs Next.js runtime-only (
cookies(),headers(),redirect(),server-only) et logique métier réutilisable. - Quand l’éviter : petits modules purement runtime sans logique métier notable, ou fonctions triviales sans intérêt à être testées séparément.
- Avantage :
- garde la logique métier importable dans un runner Node standard
- évite que
server-onlycontamine des modules purs - facilite les tests unitaires sans mocks lourds du runtime Next.js
- clarifie la responsabilité des Server Actions et handlers serveur
- Limites / vigilance :
- demande une discipline de découpage
- peut introduire une indirection inutile si la logique extraite est réellement triviale
- les frontières d’injection doivent rester simples pour éviter un excès d’abstraction
- Validé le : 19-03-2026
- Contexte technique : Next.js / Server Actions / Node test runner / modules backend injectables
Implémentation (exemple minimal)
- réserver `import "server-only"` aux fichiers qui utilisent réellement des APIs runtime Next.js
- garder la Server Action, route handler ou module email comme couche d’orchestration fine
- extraire la logique métier pure dans une fonction ou un service sans dépendance à `cookies()`, `headers()`, `redirect()` ou `server-only`
- injecter explicitement les dépendances utiles (client DB, token, callback de redirect, logger, etc.)
- tester unitairement le module pur dans le runner Node ; tester l’orchestrateur plus légèrement
Checklist
server-onlyabsent des modules de logique pure- APIs Next.js runtime-only limitées aux couches d’entrée
- Logique métier principale testable sans runtime Next.js
- Dépendances injectées explicitement quand utile
- Server Action ou handler fin et lisible
Pattern : Guardrails multi-tenant — 403 vs 404 selon la sémantique
- Objectif : éviter les fuites d’information inter-tenant tout en gardant une sémantique d’erreur claire.
- Contexte : API multi-tenant avec ressources métier isolées et surfaces internes ou opérateur.
- Quand l’utiliser : dès qu’une vérification d’appartenance tenant peut soit refuser explicitement l’accès, soit masquer l’existence d’une ressource.
- Quand l’éviter : contexte mono-tenant ou endpoints purement internes sans enjeu de fuite.
- Avantage :
- clarifie la convention de sécurité
- évite les réponses incohérentes selon les modules
- facilite les tests d’isolation tenant
- Limites / vigilance :
- la convention doit être documentée et appliquée partout
- un mauvais choix entre 403 et 404 peut révéler une information sensible
- Validé le : 16-03-2026
- Contexte technique : API multi-tenant / HTTP / services métier
Implémentation (exemple minimal)
- `assertTenantMatch(actor, expectedTenantId)` -> 403 quand la ressource est connue mais l’accès refusé
- `assertResourceBelongsToTenant(actor, resourceTenantId)` -> 404 quand il faut masquer l’existence d’une ressource d’un autre tenant
- documenter la convention dans le module
- couvrir les deux sémantiques par des tests dédiés
Checklist
- Convention 403 vs 404 documentée
- Helpers distincts selon la sémantique métier
- Aucune fuite d’existence cross-tenant sur les ressources métier
- Tests dédiés sur les deux comportements
Pattern : Repository tenant-aware — tenantId obligatoire dans la signature
- Objectif : rendre impossible par construction une query non scopée sur un domaine multi-tenant.
- Contexte : repositories ou services d’accès aux données sur ressources tenant-scoped.
- Quand l’utiliser : dès qu’un domaine métier est massivement filtré par tenant.
- Quand l’éviter : domaines réellement globaux ou méthodes volontairement cross-tenant.
- Avantage :
- force le scoping dès la signature TypeScript
- réduit les oublis de filtre tenant dans les call sites
- rend les exceptions cross-tenant visibles
- Limites / vigilance :
- les exceptions cross-tenant doivent être rares et documentées explicitement
- ne dispense pas d’un second garde-fou dans les mutations sensibles
- Validé le : 16-03-2026
- Contexte technique : TypeScript / Prisma / architecture repository
Implémentation (exemple minimal)
- chaque méthode métier tenant-scoped prend `tenantId` en paramètre obligatoire
- les méthodes réellement cross-tenant sont nommées et documentées comme exception
- les call sites Prisma directs sur ces domaines sont interdits ou supprimés
Checklist
tenantIdobligatoire sur les méthodes tenant-scoped- Exceptions cross-tenant documentées
- Appels directs concurrents à Prisma supprimés
- Tests sur scoping tenant au niveau repository
Pattern : Défense en profondeur — inclure tenantId dans les updates
- Objectif : éviter une mutation cross-tenant même si un identifiant a été mal résolu en amont.
- Contexte :
updateouupdateManysur une ressource tenant-scoped. - Quand l’utiliser : dès qu’une mutation dépend d’un
idreçu ou résolu dans un flux multi-tenant. - Quand l’éviter : ressources globales non liées à un tenant.
- Avantage :
- ajoute une seconde barrière côté base
- réduit l’impact d’un call site mal scopé
- rend la mutation plus sûre sans complexité forte
- Limites / vigilance :
- ne remplace pas le scoping en lecture ni la vérification d’autorisation
- suppose que
tenantIdsoit disponible au moment de la mutation
- Validé le : 16-03-2026
- Contexte technique : Prisma / multi-tenant / mutations métier
Implémentation (exemple minimal)
- préférer `where: { id, tenantId }` à `where: { id }` sur les updates tenant-scoped
- appliquer la même règle sur `updateMany` et opérations de révocation
- conserver les vérifications métier amont, mais ne pas leur déléguer toute la sécurité
Checklist
tenantIdprésent dans les clauseswheredes updates sensibles- Pas de mutation tenant-scoped basée sur
idseul - Revue explicite des exceptions documentées
Pattern : Next.js server-only & Server Actions — règles d'isolation
- Objectif : permettre les tests unitaires Node tout en gardant les contraintes runtime Next.js là où elles sont nécessaires.
- Contexte : monorepo Next.js App Router avec logique métier testée en Node runner natif.
- Quand l'utiliser : dès qu'un module mixe logique pure et dépendances runtime Next.js.
- Quand l'éviter : modules purement UI côté client.
- Avantage :
- logique pure testable sans friction (runner Node natif)
- Server Action fine et lisible — orchestration uniquement
server-onlyexplicite et intentionnel, pas par habitude
- Limites / vigilance :
- ne pas mettre
server-onlydans les repositories purs — casse les tests Node hors Next.js
- ne pas mettre
- Validé le : 16-03-2026
- Contexte technique : Next.js App Router / Node.js test runner
Règles
- `server-only` uniquement sur les modules qui appellent des APIs Next.js runtime
(cookies(), headers(), redirect()) — pas sur les repositories ni la logique pure
- Logique pure extraite dans un module injectable sans `server-only` :
deleteSession({ prismaClient, sessionToken })
→ testable avec le runner Node sans friction
- Server Action = orchestration mince, elle appelle les modules purs injectés
et gère les dépendances Next.js runtime uniquement
- Logique de validation / sanitisation (safeHttpUrl, etc.) → module utilitaire séparé,
sans import nodemailer / server-only
Checklist
server-onlyabsent des repositories et modules de logique pure- Server Action ≤ 10 lignes, délègue au module pur injectable
- Modules purs couverts par des tests
.spec.tsNode sans config spéciale - La logique pure ne dépend pas du runtime pour être exécutée
Pattern : Opérations auth sensibles — atomiques, idempotentes et cohérentes
- Objectif : garantir que les opérations multi-étapes auth (reset, logout, révocation) ne laissent jamais un état incohérent.
- Contexte : tout flux auth qui combine plusieurs writes : hash de mot de passe, invalidation de token, suppression de session.
- Quand l'utiliser : systématiquement sur toute opération qui touche plusieurs tables auth en séquence.
- Quand l'éviter : opérations de lecture pure.
- Avantage :
- pas de token valide après reset de mot de passe si l'opération est interrompue
- suppression de session idempotente (P2025 absorbé silencieusement)
- comportement prévisible même en cas de retry ou de concurrence
- Limites / vigilance :
$transactionPrisma ne couvre pas les effets de bord réseau (email, cookies) — ces étapes restent hors transaction
- Validé le : 16-03-2026
- Contexte technique : Node.js / Prisma / auth par session ou token
Implémentation (exemple minimal)
// consumePasswordReset — atomique dans une transaction
await prisma.$transaction([
prisma.passwordResetToken.update({
where: { tokenHash },
data: { consumedAt: new Date() },
}),
prisma.user.update({
where: { id: userId },
data: { passwordHash: newHash },
}),
prisma.session.deleteMany({ where: { userId } }),
]);
// Suppression de session — idempotente (P2025 absorbé)
try {
await prisma.session.delete({ where: { sessionToken } });
} catch (err) {
if (err?.code !== 'P2025') throw err; // session déjà supprimée → OK
}
Checklist
- Toute opération hash + update + delete dans une
$transaction P2025absorbé silencieusement sur les suppressions de session- Effets de bord hors transaction documentés (cookie, email)
- Tests couvrant le cas d'une session déjà expirée
Pattern : Réponse HTTP 200 avec payload métier pour les états d'accès
- Objectif : éviter les codes 4xx pour des états métier normaux qui nécessitent un rendu côté client.
- Contexte : endpoints dont la réponse varie selon les droits ou l'état d'abonnement, sans que l'absence de contenu soit une erreur.
- Quand l'utiliser : paywall, trial read-only, quota soft, état d'accès partiel — quand le client doit décider du rendu.
- Quand l'éviter : accès réellement interdit côté serveur (403), non authentifié (401), endpoint inexistant (404).
- Avantage :
- pas de gestion d'exception côté client mobile pour des états courants
- rendu conditionnel (paywall, teaser, empty) piloté par le payload
- log serveur propre — 4xx réservés aux erreurs techniques/sécurité
- Limites / vigilance :
- ne pas généraliser aux vraies erreurs de sécurité — 401/403/404 gardent leur sémantique HTTP
- Validé le : 20-03-2026
- Contexte technique : NestJS / Expo React Native — app-alexandrie story 4.1
Implémentation (exemple minimal)
// GET /community/forums
// Sans abonnement → 200 + { data: { forums: [], paywallRequired: true }, meta }
// Avec abonnement → 200 + { data: { forums: [...], paywallRequired: false }, meta }
// ❌ Anti-pattern
return res.status(402).json({ error: { code: 'SUBSCRIPTION_REQUIRED' } });
// ✅ Pattern correct
return res.status(200).json({
data: { forums: [], paywallRequired: true },
meta: { total: 0 },
});
Règle
- 4xx = erreur technique ou de sécurité (401 non authentifié, 403 accès interdit, 404 introuvable)
- 200 + flag métier = état métier normal que le client doit interpréter pour le rendu
Pattern : Quota journalier Redis atomique (INCR + EXPIREAT pipeline)
- Objectif : implémenter un quota d'action journalier sans race condition ni clé TTL orpheline.
- Contexte : quota par utilisateur sur une fenêtre calendaire UTC (posts, requêtes, actions sensibles).
- Quand l'utiliser : toute limite d'action journalière avec Redis disponible.
- Quand l'éviter : si Redis est down — prévoir un mode dégradé permissif (voir implémentation).
- Avantage :
- atomicité garantie :
INCR + EXPIREATdans un pipelineMULTI/EXEC - pas de clé sans TTL même en cas de deux requêtes simultanées (
count === 1concurrent) - mode dégradé explicite si Redis down (
count === null→ permissif)
- atomicité garantie :
- Limites / vigilance :
- compensation
incrBy(-1)en cas de dépassement — ne couvre pas les crashes entre INCR et la vérification - la fenêtre expire à minuit UTC, pas à minuit local
- compensation
- Validé le : 20-03-2026
- Contexte technique : Redis / NestJS / app-alexandrie story 4.2
Implémentation (exemple minimal)
// RedisService — méthode dédiée
async incrWithExpireAt(key: string, expireAtMs: number): Promise<number | null> {
const pipeline = this.client.multi();
pipeline.incr(key);
pipeline.expireAt(key, Math.floor(expireAtMs / 1000));
const results = await pipeline.exec();
return results[0] as number; // valeur post-INCR
}
// Service métier
const today = new Date().toISOString().split('T')[0]; // yyyy-mm-dd UTC
const midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
const quotaKey = `app:quota:post:${userId}:${today}`;
const count = await redis.incrWithExpireAt(quotaKey, midnight.getTime());
if (count !== null && count > QUOTA_MAX) {
await redis.incrBy(quotaKey, -1); // compensation
throw new HttpException({ error: { code: 'QUOTA_EXCEEDED' } }, HttpStatus.TOO_MANY_REQUESTS);
}
// count === null → Redis down → mode dégradé permissif
Checklist
- Vérifier le quota AVANT la création en DB
INCR + EXPIREATdans un pipeline atomique- Mode dégradé permissif si
count === null(Redis down) - Clé nommée
{app}:quota:{action}:{userId}:{yyyy-mm-dd}(date UTC) - Anti-pattern évité :
incrBy+setExséparés (race condition si count === 1 concurrent)
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)
// 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
Pattern : Sérialiser les champs Decimal Prisma en string au niveau du repository
- Objectif : éviter que les objets
DecimalPrisma traversent les couches et causent des erreurs de sérialisation JSON silencieuses. - Contexte : tout champ
Decimalen Prisma (ex:price) retourné via API ou Server Action. - Quand l'utiliser : systématiquement sur tout champ
Decimaldans les repositories. - Risque si ignoré :
Decimaln'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
// Repository — convertir avant de retourner
return {
...dish,
price: dish.price?.toString() ?? null, // Decimal → string
};
// DTO public
type DishDto = {
price: string | null; // pas Decimal
};
Pattern : Extraire les helpers de résolution tenant dans un module partagé dédié
- Objectif : éviter les couplages sémantiques incorrects entre domaines en centralisant les utilitaires transverses tenant.
- Contexte : toute fonction de résolution de tenant utilisée par plusieurs domaines métier.
- Quand l'utiliser : dès qu'un helper est importé par plus d'un module métier.
- Risque si ignoré : un module métier devient dépendance implicite d'un autre domaine distinct.
- Validé le : 17-03-2026
- Contexte technique : Next.js / TypeScript — app-template-resto
Implémentation
// ✅ src/server/tenant/resolvePublicTenant.ts
export function resolvePublicTenantSelection(env: NodeJS.ProcessEnv) { ... }
// ✅ Rétrocompatibilité depuis l'ancien emplacement si nécessaire
export { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant";
Pattern : Helper centralisé d'activation de features tenant-scoped
- Objectif : centraliser la logique d'activation/désactivation de pages ou modules par tenant dans un helper pur.
- Contexte : app multi-tenant avec features activables (pages publiques, modules optionnels, intégrations).
- Quand l'utiliser : dès qu'une feature peut être activée/désactivée par tenant.
- Avantage :
- helper pur et testable sans I/O
- comportement par défaut sain (
null/undefined→ tout activé) - composants de navigation et pages importent ce helper, jamais Prisma directement
- Validé le : 17-03-2026
- Contexte technique : Next.js App Router / TypeScript — app-template-resto
Implémentation
// src/server/public/publicPagesConfig.ts
export function isPublicPageEnabled(
config: PublicPagesConfigRecord | null | undefined,
pageKey: PublicPageKey
): boolean {
if (!config) return true; // config absente = tout activé par défaut
return config[PAGE_KEY_TO_CONFIG_FIELD[pageKey]];
}
Règle : null/undefined → tout activé. Évite les régressions si la config n'a pas été provisionnée.
Pattern : Réutiliser un champ existant plutôt que créer un modèle dédié en V1
- Objectif : éviter la sur-ingénierie en V1 en réutilisant un champ existant quand le besoin est simple.
- Contexte : early-stage, besoin de stocker une configuration simple (URL, flag, valeur unique).
- Quand l'utiliser : quand la donnée a le même cycle de vie qu'un modèle existant et ne nécessite pas de relations.
- Quand l'éviter : si la configuration a son propre cycle de vie, des cardinalités multiples, ou des relations distinctes.
- Avantage : zéro migration supplémentaire, zéro scope creep
- Validé le : 17-03-2026
- Contexte technique : Prisma / Node.js — app-template-resto
Règle
- Avant de créer un modèle ReservationConfig, vérifier si PublicHomeProfile.reservationUrl suffit
- Un champ optionnel dans le modèle le plus proche est suffisant en V1
- Ne créer un modèle dédié que si : cycle de vie distinct, relations, ou cardinalités multiples
Pattern : Valider le protocole d'une URL externe avant de la passer à un lien public
- Objectif : prévenir les injections
javascript:et URLs malformées dans les<a href>ou<img src>publics. - Contexte : toute URL venant d'une config tenant, DB ou saisie utilisateur, rendue dans le HTML.
- Quand l'utiliser : systématiquement sur tout champ URL libre stocké en DB et rendu côté HTML.
- Risque si ignoré : injection
javascript:, URL malformée, potentiel XSS. - Validé le : 17-03-2026
- Contexte technique : Node.js / Next.js — app-template-resto
Implémentation
function isSafeUrl(url: string): boolean {
try {
const { protocol } = new URL(url);
return protocol === "https:" || protocol === "http:";
} catch {
return false;
}
}
// Validation complète en service/repository
if (mediaUrl) {
try { new URL(mediaUrl); } catch { throw new HttpError("URL invalide.", { status: 400 }); }
if (!mediaUrl.startsWith("https://") && !mediaUrl.startsWith("http://"))
throw new HttpError("URL doit commencer par https://.", { status: 400 });
if (mediaUrl.length > 500)
throw new HttpError("URL trop longue.", { status: 400 });
}
// Retourner null si invalide — le composant gère l'absence d'URL
Checklist
- Validation format (
new URL()) + protocole + longueur max - Retourner
nullsi invalide, jamais passer la string brute - Composant UI reçoit
string | null, jamais une string non vérifiée
Pattern : Utilitaires purs — extraire dans un module sans server-only
- Objectif : permettre aux repositories et aux tests d'importer la même implémentation des utilitaires purs sans friction.
- Contexte : fonctions pures (slugify, formatters, validators) utilisées par des repositories qui ont
server-only. - Quand l'utiliser : dès qu'une fonction pure est utilisée dans un repository ET dans des tests.
- Risque si ignoré : logique dupliquée dans les tests qui diverge silencieusement de l'implémentation réelle.
- Validé le : 21-03-2026
- Contexte technique : Node.js / Next.js — app-template-resto
Implémentation
src/server/menuAdmin/
allergensRepository.ts ← import { slugify } from "./slugify"
slugify.ts ← export function slugify() {} // pas de "server-only"
tests/
allergens-admin.test.ts ← import { slugify } from "../src/server/menuAdmin/slugify.ts"
Pattern : EN enforcement optionnel par tenant (toggle + publish gate)
- Objectif : permettre à un tenant d'activer l'obligation de remplir les champs traduits EN, avec une gate à la publication.
- Contexte : app multi-tenant avec internationalisation optionnelle.
- Quand l'utiliser : dès qu'un tenant peut choisir d'activer/désactiver une exigence de contenu i18n.
- Validé le : 21-03-2026
- Contexte technique : Prisma / Next.js App Router — app-template-resto
Implémentation
// 1. Modèle Tenant
enableEn Boolean @default(false)
// 2. Vérification dans chaque action mutante (create/update)
const { enableEn } = await getEnConfig(tenantId);
if (enableEn && !labelEn) throw new HttpError("Traduction EN requise.", { status: 400 });
// 3. Gate publish — vérification de complétude
const result = await checkEnCompleteness(tenantId); // 4 requêtes en Promise.all
// Exclut : isSystem:true, tenantId:null, isVisible:false
if (!result.complete) throw new HttpError("Contenu EN incomplet.", { status: 422 });
Règles :
isVisible: falsen'est pas inclus dans le check (une entité masquée ne bloque pas la publication)revalidatePathsur toutes les pages menu après toggle du flag (pas seulement/settings)
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 avecP3014 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
# 1. Écrire le SQL manuellement
mkdir -p prisma/migrations/<timestamp>_<nom>
# Créer migration.sql à la main
# 2. Appliquer le SQL directement en DB
npx prisma db execute --file prisma/migrations/<timestamp>_<nom>/migration.sql
# 3. Marquer la migration comme appliquée dans _prisma_migrations
npx prisma migrate resolve --applied <timestamp>_<nom>
# 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.