Introduction
Drizzle ORM révolutionne le développement TypeScript en 2026 grâce à sa légèreté, sa sécurité de types complète et ses performances natives SQL. Contrairement aux ORMs lourds comme Prisma, Drizzle génère du SQL pur optimisé sans abstraction magique, idéal pour des applications scalables comme des APIs Next.js ou des microservices. Ce tutoriel expert vous guide pas à pas pour implémenter un blog complet avec utilisateurs, posts et commentaires : schémas relationnels, CRUD avancés, transactions ACID et migrations prod-ready.
Pourquoi choisir Drizzle ? Il offre une inférence de types parfaite (intellisense sur les colonnes), supporte tous les drivers SQL (PostgreSQL, MySQL, SQLite) et intègre Drizzle Kit pour des migrations zéro-downtime. À la fin, vous maîtriserez des patterns experts comme les upserts conditionnels et les queries récursives. Temps estimé : 2h pour un setup productionnel. Prêt à booster vos performances DB de 300% ?
Prérequis
- Node.js 20+ et npm/yarn/pnpm
- PostgreSQL 15+ (local ou Docker :
docker run -p 5432:5432 -e POSTGRES_PASSWORD=pass postgres) - Connaissances avancées en TypeScript et SQL
- Un IDE avec intellisense (VS Code + Drizzle extension)
- Outils :
npx drizzle-kit@latestpour migrations
Installation des dépendances
mkdir drizzle-expert-blog && cd drizzle-expert-blog
pnpm init
pnpm add drizzle-orm postgres drizzle-kit
pnpm add -D typescript @types/node tsx
pnpm add -D @types/pg
pnpm tsxOn crée un projet minimal et installe Drizzle ORM, le driver PostgreSQL et Drizzle Kit pour les migrations. tsx permet d'exécuter du TS nativement. Évitez npm si possible : pnpm est 3x plus rapide pour les monorepos.
Configuration initiale de Drizzle Kit
Drizzle Kit gère les schémas, générations de migrations et introspections. Créez drizzle.config.ts pour pointer vers votre DB et schéma. Utilisez push:postgres://... pour dev rapide (sans fichiers migrations) ou generate pour prod.
Fichier de configuration Drizzle
import type { Config } from 'drizzle-kit';
export default {
schema: './src/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: 'postgresql://postgres:pass@localhost:5432/drizzle_blog',
},
} satisfies Config;Ce config cible PostgreSQL local, génère les migrations dans ./drizzle. L'URL inclut DB drizzle_blog (créez-la : createdb drizzle_blog). Pour prod, utilisez des vars d'env comme process.env.DATABASE_URL. Piège : oubliez satisfies Config pour la validation TS.
Définition du schéma de base (tables Users et Posts)
import { pgTable, serial, text, timestamp, integer, pgEnum } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const statusEnum = pgEnum('post_status', ['DRAFT', 'PUBLISHED', 'ARCHIVED']);
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull().$defaultFn(() => 'Anonymous'),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
status: statusEnum('status').default('DRAFT'),
authorId: integer('author_id').references(() => users.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));Schéma type-safe avec enums, defaults, foreign keys et relations explicites via relations(). Les relations infèrent les types pour joins automatisés. Piège : references(() => users.id) pour éviter circular deps ; sans ça, TS échoue à la compilation.
Génération et push des migrations
Exécutez npx drizzle-kit generate:pg pour créer les SQL migrations, puis npx drizzle-kit push:pg pour appliquer en dev. En prod, intégrez à CI/CD avec migrate custom.
Application des migrations
npx drizzle-kit generate:pg
npx drizzle-kit push:pg
npx drizzle-kit studioGénère et pushe le schéma en DB. studio ouvre un GUI Drizzle pour explorer (comme pgAdmin mais TS-aware). Vérifiez : 2 tables créées avec FK. Piège : si DB existe, push altere sans wipe ; utilisez drop en dev si besoin.
Connexion à la base de données
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema.js';
const pool = new Pool({
connectionString: 'postgresql://postgres:pass@localhost:5432/drizzle_blog',
});
export const db = drizzle(pool, { schema });Pool PG lightweight pour prod (connexions réutilisées). drizzle(pool, { schema }) active l'intellisense sur toutes les tables. Utilisez async pour queries. Piège : oubliez export * as schema sinon types perdus.
Implémentation des opérations CRUD avancées
Passons aux queries : selects avec joins, inserts en batch, updates conditionnels et deletes en cascade. Drizzle excelle en composabilité SQL-like sans boilerplate.
Exemple CRUD complet sur Users et Posts
import { db } from './db.js';
import { users, posts, statusEnum } from './schema.js';
import { eq, and, ilike, asc, desc } from 'drizzle-orm';
import { relations } from 'drizzle-orm';
// INSERT batch avec relations
await db.insert(users).values([
{ name: 'Alice', email: 'alice@email.com' },
{ name: 'Bob', email: 'bob@email.com' },
]);
const aliceId = 1;
await db.insert(posts).values([
{ title: 'Premier post', content: 'Contenu...', authorId: aliceId },
{ title: 'Deuxième', status: 'PUBLISHED', authorId: aliceId },
]);
// SELECT avec join relations (type-safe)
const userPosts = await db
.select({
userName: users.name,
postTitle: posts.title,
postStatus: posts.status,
})
.from(users)
.leftJoin(posts, eq(users.id, posts.authorId))
.where(and(eq(users.id, aliceId), eq(posts.status, statusEnum.enumValues[1]))); // PUBLISHED
// UPDATE conditionnel
await db
.update(posts)
.set({ status: 'ARCHIVED' })
.where(ilike(posts.title, '%Premier%'));
// DELETE avec cascade (via FK ON DELETE CASCADE)
await db.delete(posts).where(eq(posts.authorId, 999)); // N'existe pas, safe
console.log('User posts:', userPosts);CRUD full-stack : batch inserts, joins via leftJoin (inclus relations pour nested selects), where composables (and, ilike). Types inférés : userPosts est {userName: string, ...}. Piège : enumValues[1] pour valeurs runtime ; utilisez eq(posts.status, 'PUBLISHED') directement.
Ajout de la table Comments avec relations many-to-many
import { pgTable, serial, text, integer, timestamp } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const comments = pgTable('comments', {
id: serial('id').primaryKey(),
content: text('content').notNull(),
postId: integer('post_id').references(() => posts.id, { onDelete: 'cascade' }),
authorId: integer('author_id').references(() => users.id),
createdAt: timestamp('created_at').defaultNow(),
});
export const commentsRelations = relations(comments, ({ one }) => ({
post: one(posts, { fields: [comments.postId], references: [posts.id] }),
author: one(users, { fields: [comments.authorId], references: [users.id] }),
}));
export const postsCommentsRelations = relations(posts, ({ many }) => ({
comments: many(comments),
}));Extension schéma : comments avec FK bidirectionnelles et onDelete: 'cascade'. Relations chainées pour deep queries. Re-générez migrations après ajout. Piège : references doit matcher exactement, sinon integrity error.
Queries avancées avec relations nested et transactions
import { db } from './db.js';
import { users, posts, comments } from './schema.js';
import { eq, desc } from 'drizzle-orm';
// Nested relations : post avec author ET comments (1 query)
const postWithRelations = await db.query.posts.findFirst({
where: eq(posts.id, 1),
with: {
author: true,
comments: {
with: { author: true },
orderBy: desc(comments.createdAt),
},
},
});
// Transaction ACID : insert post + comments atomique
await db.transaction(async (tx) => {
const newPostId = await tx.insert(posts).values({
title: 'Post transactionnel',
authorId: 1,
}).returning({ id: posts.id });
await tx.insert(comments).values([
{ content: 'Premier com', postId: newPostId[0].id, authorId: 2 },
{ content: 'Deuxième com', postId: newPostId[0].id, authorId: 1 },
]);
});
console.log('Post complet:', JSON.stringify(postWithRelations, null, 2));Queries relationnelles récursives en 1 roundtrip DB via query.posts.findFirst({ with }). Transactions explicites pour cohérence (rollback auto sur erreur). Perf : 90% moins de queries vs loops manuels. Piège : returning() pour IDs post-insert en transaction.
Bonnes pratiques
- Vars d'env pour DB URL : Toujours
process.env.DATABASE_URLavec pooling (pg.Pool). - Indexes explicites : Ajoutez
index('idx_posts_author')sur FK pour queries rapides. - Paginatez : Utilisez
limit(10).offset(page * 10)+count()séparé. - Upserts :
insert().values().onConflictDoUpdate()pour idempotence. - Prepared statements : Drizzle les gère natif ; évitez raw SQL sauf analytics.
Erreurs courantes à éviter
- Circular relations : Utilisez
eq(table.col, other.col)dans joins manuels sirelations()boucle. - Migrations manuelles : Ne touchez jamais SQL généré ;
pushen dev,migrateen prod. - Pool exhaustion : Limitez
Pool(max=20)et fermez en serverless (Next.js). - Enums string :
pgEnumstocke strings ; mismatch casusenumValues.
Pour aller plus loin
- Docs officielles : Drizzle ORM
- Kit avancé : Drizzle Zod pour validation schema → API
- Intégrez avec tRPC/Next.js : Exemple repo GitHub
- Découvrez nos formations Learni sur les bases de données avancées pour maîtriser PlanetScale/Vercel Postgres.