Lecture : 13 minutes · Niveau : intermédiaire · Mise à jour : avril 2026
Prisma est devenu l’ORM TypeScript de référence pour Node.js. Schéma déclaratif, types générés automatiquement, migrations versionnées, expérience développeur excellente. Ce guide montre comment l’utiliser efficacement en production, des bases aux patterns avancés.
Voir aussi → Node.js backend pour PME : guide pratique.
Sommaire
- Pourquoi Prisma plutôt que SQL brut ou un autre ORM
- Setup et schéma
- Requêtes CRUD de base
- Relations et inclusion
- Migrations en production
- Transactions
- Performance et N+1
- Soft delete et patterns avancés
- Limites et alternatives
- FAQ
1. Pourquoi Prisma plutôt que SQL brut ou un autre ORM
Trois alternatives principales pour interagir avec une base de données depuis Node.js :
SQL brut (pg, mysql2) : performance maximale, contrôle total, mais aucune sécurité de typage, requêtes SQL en strings dispersées dans le code, pas de migrations, pas d’autocomplétion. Pour des projets simples ou des cas où la perf brute compte.
Query builder (Knex, Kysely) : intermédiaire entre SQL brut et ORM. SQL composable en TypeScript, sans la magie d’un ORM. Plus léger que Prisma, plus de contrôle.
Prisma : ORM complet avec schéma déclaratif, types générés, migrations gérées, requêtes type-safe. Sacrifice un peu de performance pour beaucoup d’ergonomie.
Drizzle : alternative récente à Prisma. Plus proche du SQL, types générés depuis le schéma TypeScript, pas de query engine externe (Prisma utilise un binaire Rust). Adoption croissante en 2026.
Pour une PME qui démarre en 2026, Prisma reste le choix par défaut le plus productif. Drizzle est intéressant pour les équipes qui veulent plus de contrôle ou qui rencontrent les limites perf de Prisma.
2. Setup et schéma
npm install prisma @prisma/client
npx prisma init
Cela crée prisma/schema.prisma et .env. Définir le schéma :
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Client {
id String @id @default(cuid())
nom String
email String @unique
telephone String?
actif Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
@@index([email])
}
model Order {
id String @id @default(cuid())
clientId String
client Client @relation(fields: [clientId], references: [id])
total Decimal @db.Decimal(10, 2)
status OrderStatus @default(PENDING)
createdAt DateTime @default(now())
items OrderItem[]
@@index([clientId])
@@index([status])
}
enum OrderStatus {
PENDING
PAID
SHIPPED
CANCELLED
}
model OrderItem {
id String @id @default(cuid())
orderId String
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
produit String
quantite Int
prix Decimal @db.Decimal(10, 2)
}
Générer le client typé :
npx prisma generate
Créer la première migration :
npx prisma migrate dev --name init
3. Requêtes CRUD de base
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
// CREATE
const client = await prisma.client.create({
data: {
nom: "Acme SARL",
email: "contact@acme.test",
},
});
// READ
const c = await prisma.client.findUnique({
where: { id: "..." },
});
const clients = await prisma.client.findMany({
where: {
actif: true,
email: { contains: "@acme.test" },
},
orderBy: { createdAt: "desc" },
take: 20,
skip: 0,
});
// UPDATE
const updated = await prisma.client.update({
where: { id: "..." },
data: { telephone: "+221 77 123 45 67" },
});
// UPSERT (update si existe, create sinon)
const c = await prisma.client.upsert({
where: { email: "contact@acme.test" },
update: { actif: true },
create: { email: "contact@acme.test", nom: "Acme" },
});
// DELETE
await prisma.client.delete({ where: { id: "..." } });
// COUNT
const total = await prisma.client.count({ where: { actif: true } });
Toutes ces opérations sont entièrement typées. Si on essaie prisma.client.create({ data: { wrongField: "..." } }), TypeScript signale immédiatement.
Mass operations
// createMany
await prisma.client.createMany({
data: [
{ nom: "A", email: "a@test.com" },
{ nom: "B", email: "b@test.com" },
],
skipDuplicates: true,
});
// updateMany
await prisma.client.updateMany({
where: { actif: false },
data: { actif: true },
});
// deleteMany
await prisma.client.deleteMany({
where: { createdAt: { lt: new Date("2020-01-01") } },
});
4. Relations et inclusion
// Charger un client avec ses commandes
const client = await prisma.client.findUnique({
where: { id: "..." },
include: {
orders: {
where: { status: "PAID" },
orderBy: { createdAt: "desc" },
take: 10,
include: {
items: true,
},
},
},
});
// Sélectionner uniquement certains champs
const slim = await prisma.client.findUnique({
where: { id: "..." },
select: {
id: true,
nom: true,
orders: {
select: { id: true, total: true },
},
},
});
include charge l’objet complet avec ses relations. select permet de cibler précisément les champs voulus, plus économe en bande passante.
Filtrer sur les relations
// Clients qui ont au moins une commande payée
const actifs = await prisma.client.findMany({
where: {
orders: { some: { status: "PAID" } },
},
});
// Clients qui n'ont aucune commande
const sans = await prisma.client.findMany({
where: { orders: { none: {} } },
});
// Clients dont toutes les commandes sont payées
const tous = await prisma.client.findMany({
where: { orders: { every: { status: "PAID" } } },
});
5. Migrations en production
Workflow développement
# Modifier schema.prisma
npx prisma migrate dev --name add_telephone_field
# Applique la migration en dev + génère le client
Workflow production
# En CI/CD ou manuellement avant le déploiement
npx prisma migrate deploy
# Génère le client (à intégrer dans le build Docker)
npx prisma generate
migrate deploy applique les migrations sans créer de nouvelle migration ni reset la DB. C’est la commande à utiliser en production.
Bonnes pratiques
- Toujours commiter les migrations dans git, dossier
prisma/migrations/ - Ne jamais éditer une migration appliquée : créer une nouvelle migration corrective
- Tester les migrations sur staging avant la production
- Backup avant migration sur des bases critiques
- Migrations idempotentes quand possible (
IF NOT EXISTS, etc.)
Migrations risquées
Certaines migrations sont dangereuses sur des grosses tables :
ALTER TABLE ADD COLUMN NOT NULLsans valeur par défaut → bloque la tableCREATE INDEXsansCONCURRENTLY→ bloque les écrituresDROP COLUMNimmédiate → casse l’app si une version utilise encore
Décomposer en étapes :
- Ajouter colonne nullable
- Backfill (script qui remplit les valeurs)
- Ajouter contrainte NOT NULL
- Supprimer le code qui utilisait l’ancienne forme
Sur PostgreSQL : CREATE INDEX CONCURRENTLY n’est pas dans les migrations Prisma par défaut, à appliquer manuellement ou via une migration SQL custom.
6. Transactions
Pour des opérations qui doivent être atomiques :
// Transaction interactive
await prisma.$transaction(async (tx) => {
const client = await tx.client.create({ data: { nom: "A", email: "a@b" } });
await tx.order.create({
data: { clientId: client.id, total: 100 },
});
// si une erreur est jetée ici, tout est rollback
});
// Transaction batch (plus simple, opérations indépendantes)
const [created, updated] = await prisma.$transaction([
prisma.client.create({ data: {...} }),
prisma.client.update({ where: {...}, data: {...} }),
]);
Niveau d’isolation
await prisma.$transaction(
async (tx) => { /* ... */ },
{ isolationLevel: "Serializable" },
);
À utiliser pour des cas critiques (transferts financiers, compteurs concurrents). ReadCommitted (défaut) suffit dans 95% des cas.
7. Performance et N+1
Le problème N+1
// Mauvais : 1 requête + N requêtes
const clients = await prisma.client.findMany();
for (const client of clients) {
const orders = await prisma.order.findMany({ where: { clientId: client.id } });
// ...
}
// Si 100 clients, ça fait 101 requêtes
// Bon : 1 seule requête avec include
const clients = await prisma.client.findMany({
include: { orders: true },
});
Prisma gère intelligemment ce cas : include génère 2 requêtes (clients + leurs orders) avec un IN clause, pas N+1.
Pagination
Pour des grosses tables :
// Pagination par offset (simple mais inefficace sur gros datasets)
const page = await prisma.client.findMany({
skip: 1000,
take: 50,
});
// Pagination par cursor (préférable sur gros datasets)
const page = await prisma.client.findMany({
take: 50,
cursor: { id: lastSeenId },
skip: 1, // skip le cursor lui-même
});
Lecture seule, raw SQL
Pour des requêtes complexes (analytics, agrégats) :
const result = await prisma.$queryRaw`
SELECT date_trunc('day', "createdAt") as jour,
COUNT(*) as total,
SUM(total) as ca
FROM "Order"
WHERE "createdAt" >= ${dateDebut}
GROUP BY jour
ORDER BY jour DESC
`;
$queryRaw reste type-safe avec template literals (protection contre injection SQL).
Connection pooling
En production avec plusieurs instances : utiliser PgBouncer ou Prisma Accelerate pour gérer le pool de connexions. Une instance Node.js peut épuiser les connexions PostgreSQL si chacune en ouvre 10.
8. Soft delete et patterns avancés
Soft delete
Prisma n’a pas de soft delete natif. Pattern classique :
model Client {
id String @id @default(cuid())
// ...
deletedAt DateTime?
}
// Filtre par défaut (à appliquer manuellement)
const clients = await prisma.client.findMany({
where: { deletedAt: null, ...autresFiltres },
});
// Suppression
await prisma.client.update({
where: { id },
data: { deletedAt: new Date() },
});
Une extension Prisma comme prisma-extension-soft-delete factorise le filtre automatique.
Audit trail
model AuditLog {
id String @id @default(cuid())
userId String
action String // "CREATE_CLIENT", "UPDATE_ORDER", etc.
resource String
details Json?
createdAt DateTime @default(now())
}
Wrapper les opérations critiques pour logger qui a fait quoi. Soit dans le code applicatif, soit via des triggers SQL pour les cas vraiment critiques.
Multi-tenant
Pour des SaaS multi-locataires : ajouter une colonne tenantId à toutes les tables, et filtrer systématiquement. Une extension Prisma ou un middleware applicatif peut forcer ce filtrage pour éviter les fuites entre tenants.
9. Limites et alternatives
Prisma n’est pas parfait. Limitations connues :
- Performance : la sérialisation/désérialisation entre le query engine Rust et Node ajoute du surcoût. Pour des cas où chaque ms compte : Drizzle ou SQL brut.
- Requêtes complexes : certains JOINs ou window functions sont impossibles sans
$queryRaw. - Migration de gros schemas : la commande
migrate devpeut être lente sur de très gros schémas. - Bundle size : le client Prisma est lourd, problématique pour des usages serverless edge.
Drizzle comme alternative
// Schema en TypeScript
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
export const clients = pgTable("clients", {
id: text("id").primaryKey(),
nom: text("nom").notNull(),
email: text("email").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// Requête
const all = await db.select().from(clients).where(eq(clients.actif, true));
Plus proche du SQL, plus performant, plus léger. Choix moderne en 2026, surtout pour des projets edge ou ultra-perf.
Quand garder Prisma quand même
Pour la quasi-totalité des PME, les bénéfices de Prisma (DX, tooling, ressources, communauté) dépassent ses limitations. Ne pas changer juste par perfectionnisme technique.
Voir aussi → Node.js déploiement production pour les bonnes pratiques de déploiement avec Prisma.
10. FAQ
PostgreSQL ou MySQL avec Prisma ?
PostgreSQL généralement, plus mature, plus de fonctionnalités (JSON, arrays, full-text search), meilleur support Prisma. MySQL acceptable si l’équipe est déjà habituée ou si l’hébergement impose. Différences pratiques limitées pour la plupart des cas PME.
Comment debugger les requêtes générées ?
Activer le logging :
const prisma = new PrismaClient({
log: ["query", "info", "warn", "error"],
});
Affiche chaque requête SQL générée et sa durée. Indispensable quand quelque chose semble lent ou inattendu.
Prisma fonctionne-t-il en serverless ?
Oui mais avec précautions. Le démarrage de Prisma a un coût (initialisation du query engine). Sur Lambda : utiliser @prisma/adapter-pg avec un pool de connexions (PgBouncer, Prisma Accelerate, Neon serverless driver). Pour Cloudflare Workers : Drizzle est mieux adapté, le client Prisma standard ne fonctionne pas en edge.
Comment seed la base de données ?
// prisma/seed.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
await prisma.client.createMany({
data: [
{ nom: "Demo 1", email: "demo1@test.com" },
{ nom: "Demo 2", email: "demo2@test.com" },
],
});
}
main().finally(() => prisma.$disconnect());
package.json :
"prisma": { "seed": "tsx prisma/seed.ts" }
Lancement : npx prisma db seed.
Faut-il toujours utiliser des migrations ?
Pour la production : oui obligatoirement. Pour le développement local sur un projet personnel : prisma db push synchronise le schéma directement, plus rapide pour itérer mais sans historique. Ne jamais utiliser db push en production.
Comment gérer plusieurs schémas dans une même DB ?
Prisma supporte les multi-schemas (PostgreSQL) avec schemas = ["public", "auth"] dans la datasource. Utile pour isoler des modules dans la même base.
Quel est le coût de Prisma Accelerate ?
Service payant de Prisma pour cache distant et connection pooling managé. Tier gratuit disponible mais limité. Pour la plupart des PME, PgBouncer auto-hébergé ou un pooler intégré au cloud (RDS Proxy, Neon pooling) est suffisant et gratuit.
Articles liés (cluster Node.js backend)
- 👉 Node.js backend pour PME : guide pratique (pillar)
- 👉 Node.js Fastify : tutoriel pratique
- 👉 Node.js déploiement production
Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.