ITSkillsCenter
Blog

Node.js et bases de données avec Prisma : guide pratique

13 دقائق للقراءة

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

  1. Pourquoi Prisma plutôt que SQL brut ou un autre ORM
  2. Setup et schéma
  3. Requêtes CRUD de base
  4. Relations et inclusion
  5. Migrations en production
  6. Transactions
  7. Performance et N+1
  8. Soft delete et patterns avancés
  9. Limites et alternatives
  10. 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 NULL sans valeur par défaut → bloque la table
  • CREATE INDEX sans CONCURRENTLY → bloque les écritures
  • DROP COLUMN immédiate → casse l’app si une version utilise encore

Décomposer en étapes :

  1. Ajouter colonne nullable
  2. Backfill (script qui remplit les valeurs)
  3. Ajouter contrainte NOT NULL
  4. 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 dev peut ê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)


Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité