Tous les articlesLe logo de Drizzle en vert citron à côté des mots Drizzle ORM, avec un panneau de code montrant une table users et une requête select typée

Pourquoi j'utilise Drizzle pour chaque backend TypeScript

Le SQL brut n'a pas de types, et les ORM lourds cachent le SQL. Drizzle se place entre les deux : vous écrivez du TypeScript qui se lit comme du SQL, et chaque requête est typée à partir de votre schéma.

Dans cet article
  1. Qu’est-ce que Drizzle ?
  2. Drizzle et ses alternatives
  3. Pourquoi Drizzle est si bon
  4. Les types viennent du schéma
  5. Si vous connaissez le SQL, vous connaissez Drizzle
  6. Pas de magie
  7. Il tourne partout
  8. Drizzle Studio
  9. Installer Drizzle avec PostgreSQL
  10. Étape 1 : installer les dépendances
  11. Étape 2 : définir la chaîne de connexion
  12. Étape 3 : déclarer le schéma
  13. Étape 4 : déclarer les relations
  14. Étape 5 : créer le client de base de données
  15. Étape 6 : configurer Drizzle Kit
  16. Étape 7 : appliquer le schéma à la base
  17. Interroger la base
  18. Insérer
  19. Lire
  20. Modifier et supprimer
  21. Les jointures
  22. Les requêtes relationnelles
  23. Les transactions
  24. Bonnes pratiques
  25. Commitez vos migrations
  26. Gardez push pour le prototypage
  27. Un fichier de schéma par domaine
  28. Validez les entrées avant qu’elles n’atteignent la base
  29. Conclusion

Tout backend finit par parler à une base de données. En TypeScript, il existe deux façons courantes de le faire, et toutes deux ont un coût.

Écrire du SQL brut garde un contrôle total, mais les résultats reviennent sans types. Une colonne renommée ou une faute de frappe dans une requête ne se découvre qu’à l’exécution. Les ORM traditionnels règlent le problème des types, mais cachent le SQL derrière leurs propres abstractions, et il devient difficile de savoir quelles requêtes partent vraiment vers la base.

Dans cet article, j’explique pourquoi Drizzle ORM est devenu mon choix par défaut pour les backends TypeScript, comment il se compare aux alternatives, et comment l’installer de zéro avec PostgreSQL : le schéma, les migrations, les requêtes, les relations et les transactions.

Qu’est-ce que Drizzle ?

Drizzle ORM est un ORM TypeScript qui décrit le schéma de la base en TypeScript et propose des requêtes qui ressemblent à du SQL.

Sa philosophie tient en une phrase de sa documentation : si vous connaissez le SQL, vous connaissez Drizzle.

Drizzle repose sur trois idées :

  • Le schéma est du code TypeScript. Tables, colonnes, index et clés étrangères sont déclarés dans des fichiers .ts.
  • Les requêtes reflètent le SQL. select, from, where, join et orderBy se lisent comme le SQL qu’ils produisent.
  • Les types sont déduits. Chaque résultat de requête est typé à partir du schéma, sans étape de génération de code.

Drizzle est accompagné d’un outil, Drizzle Kit, qui génère et applique les migrations à partir du schéma.

Drizzle et ses alternatives

Outil Schéma défini dans Style de requête Idéal pour
SQL brut La base de données Des chaînes SQL Un contrôle total, sans types
Prisma Son propre langage de schéma Un client généré avec sa propre API Les équipes qui préfèrent éviter le SQL
TypeORM Des classes et des décorateurs Repositories et entités Les bases de code orientées objet
Kysely Des types TypeScript Un query builder typé Du SQL typé sans ORM
Drizzle Du code TypeScript Un builder proche du SQL et une API relationnelle Du SQL typé avec un schéma et des migrations

Chaque outil est un choix valable. Drizzle se distingue parce qu’il réunit ce qui est d’habitude réparti entre plusieurs outils : un schéma écrit en TypeScript, des migrations générées depuis ce schéma, et des requêtes qui restent proches du SQL.

Pourquoi Drizzle est si bon

Les types viennent du schéma

Source : documentation de Drizzle, Get started with PostgreSQL.

Une table est déclarée une fois, et ses types suivent :

type User = typeof users.$inferSelect;    // une ligne lue dans la table
type NewUser = typeof users.$inferInsert; // une ligne à insérer

$inferInsert sait quelles colonnes sont facultatives, comme les identifiants générés ou les colonnes avec une valeur par défaut. Ajouter une colonne à la table met les deux types à jour immédiatement.

Si vous connaissez le SQL, vous connaissez Drizzle

Source : documentation de Drizzle, Select.

Pas de nouveau langage de requête à apprendre. Une requête Drizzle et le SQL qu’elle produit se ressemblent :

await db.select().from(users).where(eq(users.id, 42));
select "id", "name", "age" from "users" where "users"."id" = $1; -- params: [42]

Les valeurs sont toujours envoyées en paramètres, jamais concaténées dans la requête, ce qui protège contre l’injection SQL.

Pas de magie

Drizzle ne génère pas de client, ne cache pas de requêtes derrière du chargement paresseux, et ne fait pas tourner son propre moteur à côté de l’application. Ce que vous écrivez est ce qui s’exécute.

Il tourne partout

Drizzle est léger et prend en charge de nombreux drivers : node-postgres, Postgres.js, Neon, Supabase, PGlite, MySQL, SQLite, Cloudflare D1 et d’autres. Le même schéma et les mêmes requêtes fonctionnent sur un serveur Node.js classique, dans des fonctions serverless et sur l’edge.

Drizzle Studio

Source : documentation de Drizzle, Drizzle Kit.

Drizzle Kit inclut un explorateur de base de données qui lit votre schéma :

npx drizzle-kit studio

C’est un moyen pratique d’inspecter et de modifier les données en développement, sans installer d’autre outil.

Installer Drizzle avec PostgreSQL

Les exemples suivent la documentation officielle, qui cible désormais Drizzle 1.0, installé depuis le tag rc. Les projets encore en version 0.x utilisent une API légèrement différente pour les relations.

Chaque extrait de code de cet article vient de la documentation officielle de Drizzle, parfois adapté aux mêmes tables users et posts. Chaque section renvoie à la page d’où il vient.

Étape 1 : installer les dépendances

Source : documentation de Drizzle, Get started with PostgreSQL.

npm i drizzle-orm@rc pg dotenv
npm i -D drizzle-kit@rc tsx @types/pg
  • drizzle-orm est l’ORM lui-même
  • pg est le driver PostgreSQL (node-postgres)
  • drizzle-kit génère et applique les migrations

Étape 2 : définir la chaîne de connexion

Source : documentation de Drizzle, Get started with PostgreSQL.

DATABASE_URL=postgres://user:password@localhost:5432/app

Ne commitez jamais ce fichier.

Étape 3 : déclarer le schéma

Source : documentation de Drizzle, Get started with PostgreSQL.

// src/db/schema.ts

import { index, integer, pgTable, text, varchar } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
    id: integer().primaryKey().generatedAlwaysAsIdentity(),
    name: varchar({ length: 255 }).notNull(),
    age: integer().notNull(),
    email: varchar({ length: 255 }).notNull().unique(),
});

export const posts = pgTable(
    'posts',
    {
        id: integer().primaryKey().generatedAlwaysAsIdentity(),
        content: text().notNull(),
        ownerId: integer('owner_id').references(() => users.id),
    },
    (table) => [index('posts_owner_idx').on(table.ownerId)]
);

Ce schéma :

  • Utilise des colonnes d’identité pour les clés primaires
  • Rend l’email unique
  • Relie chaque post à son auteur par une clé étrangère
  • Indexe owner_id, la colonne utilisée pour retrouver les posts d’un utilisateur

Le nom de la propriété sert en TypeScript, et le premier argument d’une colonne, comme 'owner_id', est le nom utilisé dans la base.

Étape 4 : déclarer les relations

Source : documentation de Drizzle, Relations.

Les clés étrangères décrivent la base. Les relations décrivent comment Drizzle doit charger les lignes liées dans les requêtes relationnelles :

// src/db/relations.ts

import { defineRelations } from 'drizzle-orm';
import * as schema from './schema';

export const relations = defineRelations(schema, (r) => ({
    users: {
        posts: r.many.posts(),
    },
    posts: {
        author: r.one.users({
            from: r.posts.ownerId,
            to: r.users.id,
        }),
    },
}));

Étape 5 : créer le client de base de données

Source : documentation de Drizzle, Get started with PostgreSQL.

// src/db/index.ts

import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { relations } from './relations';

const pool = new Pool({
    connectionString: process.env.DATABASE_URL!,
});

export const db = drizzle({ client: pool, relations });

Le reste de l’application importe db et ne crée jamais sa propre connexion.

Étape 6 : configurer Drizzle Kit

Source : documentation de Drizzle, Get started with PostgreSQL.

// drizzle.config.ts

import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
    out: './drizzle',
    schema: './src/db/schema.ts',
    dialect: 'postgresql',
    dbCredentials: {
        url: process.env.DATABASE_URL!,
    },
});

Étape 7 : appliquer le schéma à la base

Source : documentation de Drizzle, Migrations.

Drizzle Kit propose deux façons de faire principales.

Push

npx drizzle-kit push

push compare le schéma à la base et applique directement les différences, sans créer de fichiers de migration. C’est idéal pour prototyper.

Generate et migrate

npx drizzle-kit generate
npx drizzle-kit migrate

generate écrit un fichier de migration SQL dans le dossier drizzle, et migrate applique les migrations qui n’ont pas encore été jouées.

Les migrations peuvent aussi être appliquées au démarrage de l’application :

import { migrate } from 'drizzle-orm/node-postgres/migrator';

await migrate(db);

Pour tout ce qui ira en production, je recommande les migrations générées : ce sont de simples fichiers SQL qu’on peut relire et commiter avec le code.

Interroger la base

Insérer

Source : documentation de Drizzle, Insert.

import { users } from './db/schema';

const user: typeof users.$inferInsert = {
    name: 'John',
    age: 30,
    email: '[email protected]',
};

await db.insert(users).values(user);

En PostgreSQL, returning() renvoie la ligne insérée, ou seulement les colonnes demandées :

const [created] = await db.insert(users).values(user).returning();

const [{ insertedId }] = await db
    .insert(users)
    .values({ name: 'Dan', age: 25, email: '[email protected]' })
    .returning({ insertedId: users.id });

Lire

Source : documentation de Drizzle, Select.

import { eq } from 'drizzle-orm';

const allUsers = await db.select().from(users);
// { id: number; name: string; age: number; email: string }[]

const john = await db
    .select()
    .from(users)
    .where(eq(users.email, '[email protected]'));

Un select partiel ne renvoie que les colonnes demandées, et le type suit :

const emails = await db
    .select({ id: users.id, email: users.email })
    .from(users);
// { id: number; email: string }[]

Les conditions se combinent avec des fonctions comme and, or, gt et ilike. Une condition undefined est simplement ignorée, ce qui rend les filtres facultatifs faciles :

import { ilike } from 'drizzle-orm';

const searchPosts = async (term?: string) => {
    return db
        .select()
        .from(posts)
        .where(term ? ilike(posts.content, term) : undefined);
};

Modifier et supprimer

Source : documentation de Drizzle, Get started with PostgreSQL.

await db
    .update(users)
    .set({ age: 31 })
    .where(eq(users.email, '[email protected]'));

await db.delete(users).where(eq(users.email, '[email protected]'));

Les jointures

Source : documentation de Drizzle, Joins.

const result = await db
    .select()
    .from(users)
    .leftJoin(posts, eq(users.id, posts.ownerId));

Le résultat est typé, y compris le fait qu’une jointure gauche peut ne renvoyer aucun post :

const result: {
    users: { id: number; name: string; age: number; email: string };
    posts: { id: number; content: string; ownerId: number | null } | null;
}[];

Les requêtes relationnelles

Source : documentation de Drizzle, Relational queries.

Pour des données imbriquées, l’API relationnelle charge les lignes avec leurs relations en une seule requête :

const usersWithPosts = await db.query.users.findMany({
    with: {
        posts: true,
    },
});

const user = await db.query.users.findFirst({
    where: {
        id: 1,
    },
    with: {
        posts: true,
    },
});

Chaque utilisateur revient avec un tableau posts, typé à partir du schéma. findFirst() ajoute limit 1 à la requête.

Les transactions

Source : documentation de Drizzle, Transactions.

Une transaction regroupe plusieurs requêtes qui doivent réussir ou échouer ensemble. Cet exemple de la documentation transfère de l’argent entre deux comptes :

await db.transaction(async (tx) => {
    const [account] = await tx
        .select({ balance: accounts.balance })
        .from(accounts)
        .where(eq(users.name, 'Dan'));

    if (account.balance < 100) {
        // Lève une exception qui annule la transaction.
        tx.rollback();
    }

    await tx.update(accounts).set({ balance: sql`${accounts.balance} - 100.00` }).where(eq(users.name, 'Dan'));
    await tx.update(accounts).set({ balance: sql`${accounts.balance} + 100.00` }).where(eq(users.name, 'Andrew'));
});

Si une requête échoue, ou si tx.rollback() est appelé, aucune des modifications n’est conservée.

Bonnes pratiques

Commitez vos migrations

Les migrations générées font partie du code. Relisez-les comme n’importe quel changement, et commitez-les avec la modification du schéma qui les a produites.

Gardez push pour le prototypage

drizzle-kit push est rapide, mais ne laisse aucun historique. Dès qu’une base contient de vraies données, passez à generate et migrate.

Un fichier de schéma par domaine

Quand l’application grandit, découpez le schéma en plusieurs fichiers, comme users.ts, billing.ts ou posts.ts, et faites pointer schema dans la configuration vers le dossier.

Validez les entrées avant qu’elles n’atteignent la base

Source : documentation de Drizzle, drizzle-zod.

Le schéma décrit la base, pas ce que les clients ont le droit d’envoyer. Drizzle peut générer des schémas Zod à partir des tables :

import { createInsertSchema } from 'drizzle-orm/zod';

const userInsertSchema = createInsertSchema(users);

const parsed = userInsertSchema.parse(input);
await db.insert(users).values(parsed);

Les règles de validation restent synchronisées avec la table, et les données invalides sont refusées avant que la requête ne parte.

Conclusion

Drizzle donne aux backends TypeScript ce qui manque au SQL brut comme aux ORM traditionnels : le contrôle et la lisibilité du SQL, avec des types qui viennent directement du schéma.

Dans cet article, nous avons vu ce qu’est Drizzle, comment il se compare à Prisma, TypeORM et Kysely, et pourquoi il est devenu mon choix par défaut. Nous l’avons ensuite installé avec PostgreSQL, du schéma et des relations jusqu’aux migrations, aux requêtes et aux transactions.

Ce qui en fait mon choix pour chaque backend TypeScript :

  • Un schéma écrit en TypeScript, d’où les types sont déduits
  • Des requêtes qui se lisent comme le SQL qu’elles produisent
  • Des migrations générées depuis le schéma, en simples fichiers SQL
  • Une petite bibliothèque qui tourne sur un serveur, en serverless et sur l’edge

Le meilleur outil est celui qui se fait oublier. Avec Drizzle, la base reste du SQL, le code reste du TypeScript, et le compilateur vérifie que les deux sont d’accord.