Communitygithub.com

prisma-patterns

Modèles Prisma ORM pour backends TypeScript — conception de schémas, optimisation de requêtes, transactions, pagination et pièges critiques comme `updateMany` retournant un compte, les timeouts de `$transaction`, `migrate dev` réinitialisant la BD, `@updatedAt` ignoré dans les écritures groupées, et l'épuisement des connexions serverless.

Qu'est-ce que prisma-patterns ?

Ce skill fournit un guide complet des modèles de production et des pièges non évidents pour Prisma ORM dans les backends TypeScript. Il couvre la conception de schémas, les stratégies d'ID, les defaults, ainsi que des techniques de requêtage avancées comme `include` vs `select`, la pagination par curseur, et le soft delete. Il détaille également la configuration du singleton PrismaClient (incluant la gestion des adaptateurs pour les nouvelles installations), la gestion des transactions (formes tableau et interactives), et la connexion en environnement serverless. Les anti-modèles incluent `updateMany` retournant un compte et non des enregistrements, les timeouts de transaction interactive (5s), les risques de `migrate dev`, la modification manuelle des fichiers de migration, les changements de schéma en une seule étape, l'inopérabilité de `@updatedAt` sur les écritures massives, et le soft delete combiné à `findUniqueOrThrow`. Les meilleures pratiques recommandent l'utilisation de `migrate deploy` en CI/CD, le mapping des entités vers des DTOs de réponse, l'ajout d'index via `@@index` sur toutes les clés étrangères et colonnes filtrées, et la gestion explicite des filtres de soft delete sans middleware. Ce skill est adapté à toutes les versions récentes de Prisma, avec des notes spécifiques sur les différences d'API entre les versions (relationJoins, omit, Client Extensions).

Compatible avec~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/prisma-patterns

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Prisma Patterns

Modèles de production et pièges non évidents pour Prisma ORM dans les backends TypeScript.

Vérifiez votre version avant d'appliquer les modèles. La surface d'API de Prisma a évolué à travers les versions majeures :

npx prisma --version

Différences notables d'API entre les versions :

  • relationJoins peut charger les relations via JOIN plutôt que des requêtes séparées, mais peut provoquer une explosion de lignes sur des relations 1:N volumineuses ou des include profonds — évaluez les deux approches
  • Le modificateur de champ omit et l'API Client Extensions (prisma.$extends) ont été ajoutés
  • Nouvelles installations : le package peut s'appeler prisma au lieu de @prisma/client ; PrismaClient peut nécessiter un adaptateur (ex. @prisma/adapter-pg) ; datasource.url peut se trouver dans prisma.config.ts plutôt que schema.prisma
  • Les commandes CLI (migrate dev, migrate deploy, generate) sont inchangées entre les versions

Quand activer

  • Concevoir ou modifier des modèles et relations de schéma Prisma
  • Écrire des requêtes, transactions ou logique de pagination
  • Utiliser updateMany, deleteMany ou toute opération groupée
  • Exécuter ou planifier des migrations de base de données
  • Déployer dans des environnements serverless (Vercel, Lambda, Cloudflare Workers)
  • Implémenter une suppression douce ou un filtrage multi-tenant par ligne

Concepts de base

Stratégie d'ID

StratégieQuand l'utiliserQuand l'éviter
@default(cuid())Choix par défaut — URL-safe, triable, pas de collisionsIdentifiants séquentiels nécessaires pour les systèmes externes
@default(uuid())Interopérabilité requise avec des systèmes non-PrismaTables à écriture élevée (les UUID aléatoires fragmentent les index B-tree)
@default(autoincrement())Tables de jointure internes, journaux d'auditIdentifiants exposés publiquement (divulgue le nombre d'enregistrements)

Valeurs par défaut du schéma

model User {
  id        String    @id @default(cuid())
  email     String    @unique  // @unique crée déjà un index — pas besoin de @@index
  name      String
  role      Role      @default(USER)
  posts     Post[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?

  @@index([createdAt])
  @@index([deletedAt, createdAt]) // composite pour soft delete + tri
}
  • Ajoutez @@index sur chaque clé étrangère et colonne utilisée dans WHERE ou ORDER BY.
  • Déclarez deletedAt DateTime? dès le départ lorsque la suppression douce est une exigence prévisible — l'ajouter ultérieurement nécessite une migration sur une table en production.
  • updatedAt @updatedAt est défini automatiquement par Prisma uniquement sur update et upsert (voir Anti-Modèles pour le piège des mises à jour groupées).

include vs select

includeselect
RetourneTous les champs scalaires + les relations spécifiéesUniquement les champs spécifiés
Quand l'utiliserVous avez besoin de la plupart des champs plus une relationChemins chauds, grandes tables, éviter le sur-chargement
PerformancePeut sur-charger sur les tables largesCharge utile minimale, plus rapide sur les grands ensembles de données
Note Prisma 5Utilise JOIN par défaut (relationJoins)Identique
// include — toutes les colonnes + relation
const user = await prisma.user.findUnique({
  where: { id },
  include: { posts: { select: { id: true, title: true } } },
});

// select — liste d'autorisation explicite
const user = await prisma.user.findUnique({
  where: { id },
  select: { id: true, email: true, name: true },
});

Ne renvoyez jamais d'entités Prisma brutes depuis les réponses API — mappez vers des DTOs de réponse pour contrôler les champs exposés :

// MAUVAIS : fuit passwordHash, deletedAt, champs internes
return await prisma.user.findUniqueOrThrow({ where: { id } });

// BON : mappage DTO explicite
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
return { id: user.id, name: user.name, email: user.email };

Sélection de la forme de transaction

SituationUtiliser
Opérations indépendantes, sans interdépendanceForme tableau
Une étape ultérieure dépend d'un résultat précédentForme interactive
Appels externes (email, HTTP) impliquésEn dehors de la transaction complètement
// Forme tableau — regroupées en un seul aller-retour
const [user, post] = await prisma.$transaction([
  prisma.user.update({ where: { id }, data: { name } }),
  prisma.post.create({ data: { title, authorId: id } }),
]);

// Forme interactive — utilisez uniquement le client tx, jamais le client prisma externe
const post = await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({ where: { id } });
  if (user.role !== 'ADMIN') throw new Error('Interdit');
  return tx.post.create({ data: { title, authorId: user.id } });
});

Singleton PrismaClient

Chaque instance de PrismaClient ouvre son propre pool de connexions. Instanciez une seule fois.

// lib/prisma.ts

// Option A — initialisation par adaptateur (requise par les nouvelles installations de Prisma)
import { PrismaClient } from '@prisma/client'; // ou le chemin du client généré pour votre configuration
import { PrismaPg } from '@prisma/adapter-pg';

function createPrismaClient() {
  const adapter = new PrismaPg({
    connectionString: process.env.DATABASE_URL!,
  });
  return new PrismaClient({
    adapter,
    log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
  });
}

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const prisma = globalForPrisma.prisma ?? createPrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

// Option B — initialisation directe (anciennes installations, pas d'adaptateur nécessaire)
// import { PrismaClient } from '@prisma/client';
// export const prisma = globalForPrisma.prisma ?? new PrismaClient({ ... });

Utilisez l'Option A si votre installation Prisma nécessite un argument adapter dans le constructeur PrismaClient. Utilisez l'Option B si new PrismaClient() fonctionne sans arguments. Laissez le compilateur vous dire laquelle est correcte.

Le motif globalThis empêche les instances en double lors du rechargement à chaud (Next.js, nodemon, ts-node-dev).

Problème N+1

Charger les relations dans une boucle émet une requête par ligne.

// MAUVAIS : N+1 — une requête supplémentaire par utilisateur
const users = await prisma.user.findMany();
for (const user of users) {
  const posts = await prisma.post.findMany({ where: { authorId: user.id } });
}

// BON : une seule requête
const users = await prisma.user.findMany({ include: { posts: true } });

Avec relationJoins de Prisma 5+, la forme include utilise un seul JOIN. Sur de grands ensembles 1:N, cela peut augmenter la taille du jeu de résultats — évaluez les deux approches si la relation peut retourner de nombreuses lignes par parent.

Exemples de code

Pagination par curseur (recommandée pour les flux et grands ensembles de données)

async function getPosts(cursor?: string, limit = 20) {
  const items = await prisma.post.findMany({
    where: { published: true },
    orderBy: [
      { createdAt: 'desc' },
      { id: 'desc' }, // tri secondaire empêche une pagination instable sur des horodatages en double
    ],
    take: limit + 1,
    ...(cursor && { cursor: { id: cursor }, skip: 1 }),
  });

  const hasNextPage = items.length > limit;
  if (hasNextPage) items.pop();

  return { items, nextCursor: hasNextPage ? items[items.length - 1].id : null };
}

Récupérez limit + 1 et pop — méthode canonique pour détecter hasNextPage sans requête de comptage supplémentaire. Incluez toujours un champ unique (ex. id) comme orderBy secondaire pour éviter une pagination instable lorsque plusieurs lignes partagent le même horodatage. Utilisez la pagination par décalage uniquement lorsque les utilisateurs doivent sauter à des pages arbitraires (tableaux d'administration).

Suppression douce (Soft Delete)

// Filtrez toujours explicitement — ne comptez pas sur le middleware (masque le comportement, difficile à déboguer)
const activeUsers = await prisma.user.findMany({ where: { deletedAt: null } });

await prisma.user.update({ where: { id }, data: { deletedAt: new Date() } });
await prisma.user.update({ where: { id }, data: { deletedAt: null } }); // restauration

Gestion des erreurs

import { Prisma } from '@prisma/client'; // ou le chemin du client généré pour votre configuration

try {
  await prisma.user.create({ data: { email } });
} catch (e) {
  if (e instanceof Prisma.PrismaClientKnownRequestError) {
    if (e.code === 'P2002') throw new ConflictError('L\'email existe déjà');
    if (e.code === 'P2025') throw new NotFoundError('Enregistrement non trouvé');
    if (e.code === 'P2003') throw new BadRequestError('L\'enregistrement référencé n\'existe pas');
  }
  throw e;
}

Codes courants : P2002 violation d'unicité · P2025 non trouvé · P2003 violation de clé étrangère.

Capturez à la limite du service et traduisez en erreurs métier. N'exposez jamais les messages bruts de Prisma aux consommateurs de l'API.

Pool de connexions — Serverless

Intégrez les paramètres de connexion directement dans DATABASE_URL — la concaténation de chaînes échoue si l'URL a déjà des paramètres de requête (ex. ?schema=public) :

# .env — recommandé : intégrez les paramètres dans l'URL
DATABASE_URL="postgresql://user:pass@host/db?connection_limit=1&pool_timeout=20"

# Avec un pooler externe (PgBouncer, Supabase pooler)
DATABASE_URL="postgresql://user:pass@host/db?pgbouncer=true&connection_limit=1"
// Vercel, AWS Lambda et environnements serverless similaires :
// limiter le pool à 1 par instance ; connection_limit et pool_timeout contrôlés via DATABASE_URL

// Configuration par adaptateur (si votre installation Prisma nécessite un adaptateur) :
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';

const prisma = new PrismaClient({
  adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
});

// Configuration directe (si votre installation Prisma ne nécessite pas d'adaptateur) :
// const prisma = new PrismaClient();

Anti-Modèles

updateMany retourne un compte, pas des enregistrements

// MAUVAIS : résultat est { count: 2 } — users[0] est undefined
const users = await prisma.user.updateMany({ where: { role: 'GUEST' }, data: { role: 'USER' } });

// BON : capturez d'abord les IDs, puis mettez à jour, puis récupérez uniquement les lignes affectées
const targets = await prisma.user.findMany({
  where: { role: 'GUEST' },
  select: { id: true },
});
const ids = targets.map((u) => u.id);
await prisma.user.updateMany({ where: { id: { in: ids } }, data: { role: 'USER' } });
const updated = await prisma.user.findMany({ where: { id: { in: ids } } });

Idem pour deleteMany — retourne { count: n }, jamais les lignes supprimées.

La forme interactive de $transaction expire après 5 secondes

// MAUVAIS : appel externe dans la transaction dépasse la valeur par défaut de 5s → "Transaction already closed"
await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({ where: { id } });
  await sendWelcomeEmail(user.email); // appel externe
  await tx.user.update({ where: { id }, data: { emailSent: true } });
});

// BON : appels externes en dehors de la transaction
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
await sendWelcomeEmail(user.email);
await prisma.user.update({ where: { id }, data: { emailSent: true } });

// Augmentez le timeout uniquement lorsque le traitement par lots en a réellement besoin
await prisma.$transaction(async (tx) => { ... }, { timeout: 30_000 });

migrate dev peut réinitialiser la base de données

migrate dev détecte la dérive du schéma et peut demander de réinitialiser la BD, supprimant toutes les données.

# JAMAIS sur un environnement partagé, de staging ou de production
npx prisma migrate dev --name add_column

# Sûr partout sauf en développement local solo
npx prisma migrate deploy

# Vérifiez la dérive sans appliquer
npx prisma migrate diff \
  --from-migrations ./prisma/migrations \
  --to-schema-datamodel ./prisma/schema.prisma \
  --shadow-database-url "$SHADOW_DATABASE_URL"

Modifier manuellement un fichier de migration casse les déploiements futurs

Prisma vérifie la somme de contrôle de chaque fichier de migration. Le modifier après application provoque une P3006 checksum mismatch sur chaque environnement où l'original a déjà été exécuté. Créez une nouvelle migration à la place.

Les changements de schéma cassants nécessitent une migration en plusieurs étapes

Ajouter NOT NULL à une colonne existante ou renommer une colonne en une seule migration verrouillera la table ou supprimera des données. Utilisez la méthode expand-and-contract :

# Étape 1 : créer la migration localement, puis déployer
npx prisma migrate dev --name add_new_column   # local uniquement
npx prisma migrate deploy                       # staging / production
// Étape 2 : rattraper les données (exécuter dans un script ou un job de migration, pas dans le shell)
await prisma.user.updateMany({ data: { newColumn: derivedValue } });
# Étape 3 : créer la migration de contrainte NOT NULL localement, puis déployer
npx prisma migrate dev --name make_new_column_required  # local uniquement
npx prisma migrate deploy                               # staging / production

@updatedAt ne se déclenche pas sur updateMany

@updatedAt est défini automatiquement uniquement sur update et upsert. Les écritures groupées le laissent obsolète.

// MAUVAIS : updatedAt reste à son ancienne valeur
await prisma.post.updateMany({ where: { authorId }, data: { published: true } });

// BON
await prisma.post.updateMany({
  where: { authorId },
  data: { published: true, updatedAt: new Date() },
});

Soft delete + findUniqueOrThrow divulgue les enregistrements supprimés

findUniqueOrThrow lève P2025 uniquement lorsque la ligne n'existe pas dans la BD. Les lignes supprimées (soft-deleted) existent toujours et sont retournées sans erreur.

findUniqueOrThrow nécessite un champ de contrainte d'unicité dans where — ajouter deletedAt: null à côté de id casse le type car { id, deletedAt } n'est pas une contrainte d'unicité composée. Utilisez findFirstOrThrow à la place.

// MAUVAIS : retourne un utilisateur soft-deleted
const user = await prisma.user.findUniqueOrThrow({ where: { id } });

// MAUVAIS : erreur de type Prisma — { id, deletedAt } n'est pas une contrainte d'unicité
const user = await prisma.user.findUniqueOrThrow({ where: { id, deletedAt: null } });

// BON : findFirstOrThrow supporte des conditions where arbitraires
const user = await prisma.user.findFirstOrThrow({ where: { id, deletedAt: null } });

deleteMany sans where supprime toutes les lignes

// MAUVAIS : efface silencieusement la table
await prisma.post.deleteMany();

// BON
await prisma.post.deleteMany({ where: { authorId: userId } });

Bonnes pratiques

RègleRaison
migrate deploy dans CI/CD, migrate dev uniquement en localmigrate dev peut réinitialiser la BD en cas de dérive
Mapper les entités vers des DTOs de réponseEmpêche la fuite de champs internes
Ajouter @@index sur chaque clé étrangère et colonne de filtre/triLes index manquants sont la cause la plus fréquente de performances lentes de Prisma en production
Utiliser findFirstOrThrow au lieu de findUniqueOrThrow lors du filtrage par soft deletefindUniqueOrThrow nécessite une contrainte d'unicité et ne peut pas combiner avec deletedAt: null
Capturer les erreurs Prisma et les traduire en erreurs applicativesEmpêche les fuites d'informations internes, permet un traitement cohérent des erreurs
Utiliser la pagination par curseur pour les listes paginéesPlus efficace que la pagination par décalage sur les grands ensembles de données, stable même lorsque des lignes sont insérées ou supprimées
Limiter le pool de connexions à 1 en serverlessChaque fonction peut s'exécuter sur une instance séparée, plus de connexions que les limites de la base de données

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

Skills associés