
Je suis Mathieu Ponton, Co-Founder & ingénieur logiciel chez Apogée Consult à Lyon. Ingénieur diplômé de Polytech Lyon (Informatique), j'ai fait trois ans en apprentissage, partagés entre la Métropole de Lyon (inclusion numérique avec Res'in et sobriété énergétique avec Écolyo) et Superwyze, une startup medtech (POCs, dont certains aujourd'hui industrialisés, et travail sur des codebases existantes). J'ai livré plus de 10 projets en production (web, mobile et IA / RAG) pour des PME, startups et organisations publiques.
Drizzle vs Prisma après 12 mois de production : ce qui a vraiment fait pencher la balance
- archi
Nous avons migré un projet de Prisma 5 vers Drizzle ORM après 12 mois. Ce n'était pas une décision facile. Voici ce qui a vraiment pesé dans la balance, au-delà du benchmark de vitesse.
Drizzle vs Prisma après 12 mois de production : ce qui a vraiment fait pencher la balance
Les benchmarks qui circulent sur Drizzle vs Prisma mesurent le throughput de requêtes simples sur des tables vides. C'est utile pour comprendre l'overhead des deux couches, mais ça ne dit rien sur ce qui compte en production : la maintenabilité des migrations, la qualité du typage sur des queries complexes, et la gestion des cas limites qui ne rentrent pas dans l'API de l'ORM.
Voici notre retour après 12 mois sur un projet SaaS avec Prisma 5, suivi d'une migration vers Drizzle.
Pourquoi on est partis de Prisma
Prisma 5 est solide. L'expérience développeur pour les cas simples est excellente. Mais nous avons accumulé trois frictions qui ont fini par peser.
Les migrations Prisma ne sont pas idempotentes
Prisma génère des fichiers de migration SQL dans prisma/migrations/. Ces fichiers sont rejoués dans l'ordre sur chaque environnement. Si une migration échoue à mi-chemin (crash réseau, contrainte violée), l'environnement se retrouve dans un état incohérent. prisma migrate resolve permet de marquer une migration comme appliquée ou comme à-rejeter, mais c'est une opération manuelle qui demande de l'attention.
Sur deux projets différents, nous avons eu des drifts entre l'état de la base en staging et les fichiers de migration. prisma migrate diff aide à détecter ces drifts, mais la résolution reste manuelle.
Le client Prisma ne tourne pas partout
Prisma génère un client binaire natif (libquery_engine). Sur les edge runtimes (Cloudflare Workers, Vercel Edge), ce binaire n'est pas disponible. Prisma Accelerate existe pour contourner ça, mais c'est un service managé avec une couche réseau supplémentaire.
Dans notre cas, nous voulions faire tourner des Server Actions Next.js sur le Node.js runtime standard, pas sur l'Edge. Ce n'était pas un problème direct. Mais la contrainte était là, et la solution de Prisma pour les environments modernes (Accelerate, Pulse) ajoute de la dépendance externe.
Les relations profondes produisent des types opaques
Prisma infère les types depuis le schema .prisma. Pour des requêtes avec des include profonds, le type résultant est généré automatiquement mais devient rapidement difficile à nommer et à passer entre fonctions.
// Prisma, comment typer ce retour ?
const order = await prisma.order.findUnique({
where: { id },
include: {
items: { include: { product: { include: { category: true } } } },
customer: { include: { address: true } },
},
});
// Il faut utiliser Prisma.OrderGetPayload<typeof ...>, verbeux
type OrderWithDetails = Prisma.OrderGetPayload<{
include: {
items: { include: { product: { include: { category: true } } } };
customer: { include: { address: true } };
};
}>;C'est fonctionnel, mais le type n'est pas défini explicitement dans le code. Il dérive de la query. Quand on refactorise la query, le type change silencieusement.
Ce qu'on a trouvé avec Drizzle
Le schema est du TypeScript pur
Avec Drizzle, le schema est défini en TypeScript. Pas de fichier .prisma, pas de génération de client. Les types sont inférés depuis les définitions de table :
// schema.ts
export const orders = pgTable("orders", {
id: uuid("id").primaryKey().defaultRandom(),
customerId: uuid("customer_id").notNull().references(() => customers.id),
status: text("status", { enum: ["pending", "paid", "shipped"] }).notNull(),
total: numeric("total", { precision: 10, scale: 2 }).notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
});
export type Order = typeof orders.$inferSelect;
export type NewOrder = typeof orders.$inferInsert;Order est un type pur TypeScript, défini une fois, utilisable partout sans syntaxe spéciale.
Les migrations sont des fichiers SQL explicites
Drizzle Kit génère des fichiers SQL depuis le diff entre le schema TypeScript et la base. Contrairement à Prisma, on peut lire et modifier ces fichiers avant de les appliquer. Les migrations sont idempotentes si on les écrit comme telles (ce qu'on peut faire, contrairement à Prisma où le SQL généré n'est pas toujours idempotent).
-- Généré par drizzle-kit generate
ALTER TABLE "orders"
ADD COLUMN "shipped_at" timestamp;
-- On peut annoter, commenter, ou ajouter du SQL custom
-- avant d'appliquer avec drizzle-kit migrateLe SQL complexe reste lisible
Drizzle expose un builder de query qui ressemble à du SQL. Sur des queries avec CTEs, window functions, ou GROUP BY complexes, c'est plus proche du SQL réel que Prisma :
const revenueByMonth = await db
.select({
month: sql<string>`to_char(created_at, 'YYYY-MM')`,
total: sql<number>`sum(total)`,
orderCount: count(),
})
.from(orders)
.where(
and(
eq(orders.status, "paid"),
gte(orders.createdAt, startDate)
)
)
.groupBy(sql`to_char(created_at, 'YYYY-MM')`)
.orderBy(sql`to_char(created_at, 'YYYY-MM')`);Avec Prisma, cette query passerait par prisma.$queryRaw et perdrait le typage automatique.
Ce que Prisma fait encore mieux
Soyons équilibrés.
L'introspection de base existante
prisma db pull génère un schema Prisma depuis une base existante. C'est un outil puissant pour les projets de reprise sur une base legacy. Drizzle a drizzle-kit introspect qui fait quelque chose de similaire, mais la qualité du résultat est moins mature.
La documentation et la communauté
Prisma a plus de 36 000 étoiles GitHub contre environ 25 000 pour Drizzle (au moment de notre migration). La documentation Prisma est exhaustive. Pour des cas limites, on trouve plus facilement de l'aide.
L'expérience pour les requêtes CRUD simples
Sur un projet avec surtout des CRUD et peu de SQL complexe, Prisma est plus confortable. L'API est plus haut niveau et demande moins de connaissance SQL. Drizzle demande de se sentir à l'aise avec SQL.
Le bilan honnête de la migration
La migration a pris environ une semaine de travail pour un projet de taille intermédiaire (25 tables, 40 000 lignes de code TypeScript). Les étapes principales :
- Introspection de la base existante avec
drizzle-kit introspect, résultat correct à 80 %, ajustements manuels nécessaires sur les types enum et les contraintes complexes. - Réécriture des queries par fichier, la plupart sont mécaniques, certaines ont révélé des simplifications possibles.
- Vérification du comportement des transactions, Drizzle utilise des closures pour les transactions, pas les méthodes de client interactif de Prisma.
Ce qu'on a gagné : des migrations plus prévisibles, un meilleur contrôle sur le SQL généré, des types plus explicites. Ce qu'on a perdu : la vitesse de démarrage sur les cas simples, et quelques heures de documentation à lire pour les edge cases.
Drizzle est notre choix par défaut sur les nouveaux projets. Prisma reste pertinent pour des équipes moins à l'aise avec SQL ou pour des prototypes rapides. La vraie question : votre équipe veut-elle une abstraction qui cache SQL ou un outil qui reste proche de SQL ?