Skip to content
Learni
View all tutorials
Base de données

How to Master Drizzle ORM in 2026

Lire en français

Introduction

Drizzle ORM is revolutionizing TypeScript development in 2026 with its lightweight design, full type safety, and native SQL performance. Unlike heavy ORMs like Prisma, Drizzle generates pure optimized SQL without magical abstractions, making it ideal for scalable apps like Next.js APIs or microservices. This expert tutorial guides you step by step through building a complete blog with users, posts, and comments: relational schemas, advanced CRUD, ACID transactions, and production-ready migrations.

Why choose Drizzle? It delivers perfect type inference (IntelliSense on columns), supports all SQL drivers (PostgreSQL, MySQL, SQLite), and integrates Drizzle Kit for zero-downtime migrations. By the end, you'll master expert patterns like conditional upserts and recursive queries. Estimated time: 2 hours for a production setup. Ready to boost your DB performance by 300%?

Prerequisites

  • Node.js 20+ and npm/yarn/pnpm
  • PostgreSQL 15+ (local or Docker: docker run -p 5432:5432 -e POSTGRES_PASSWORD=pass postgres)
  • Advanced knowledge of TypeScript and SQL
  • An IDE with IntelliSense (VS Code + Drizzle extension)
  • Tools: npx drizzle-kit@latest for migrations

Installing Dependencies

terminal
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 tsx

Create a minimal project and install Drizzle ORM, the PostgreSQL driver, and Drizzle Kit for migrations. tsx lets you run TypeScript natively. Avoid npm if possible: pnpm is 3x faster for monorepos.

Initial Drizzle Kit Configuration

Drizzle Kit handles schemas, migration generation, and introspection. Create drizzle.config.ts to point to your DB and schema. Use push:postgres://... for fast dev (no migration files) or generate for production.

Drizzle Configuration File

drizzle.config.ts
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;

This config targets a local PostgreSQL instance and generates migrations in ./drizzle. The URL includes the drizzle_blog database (create it: createdb drizzle_blog). For production, use environment variables like process.env.DATABASE_URL. Pitfall: forget satisfies Config and you lose TypeScript validation.

Basic Schema Definition (Users and Posts Tables)

src/schema.ts
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],
  }),
}));

Type-safe schema with enums, defaults, foreign keys, and explicit relations via relations(). Relations enable automated type-safe joins. Pitfall: Use references(() => users.id) to avoid circular dependencies; otherwise, TypeScript fails at compile time.

Generating and Pushing Migrations

Run npx drizzle-kit generate:pg to create SQL migrations, then npx drizzle-kit push:pg to apply them in development. For production, integrate into CI/CD with custom migrate scripts.

Applying Migrations

terminal
npx drizzle-kit generate:pg
npx drizzle-kit push:pg
npx drizzle-kit studio

Generates and pushes the schema to the database. studio opens a Drizzle GUI explorer (like pgAdmin but TypeScript-aware). Verify: 2 tables created with foreign keys. Pitfall: If the DB exists, push alters without wiping; use drop in dev if needed.

Database Connection

src/db.ts
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 });

Lightweight PG pool for production (reuses connections). drizzle(pool, { schema }) enables IntelliSense across all tables. Use async for queries. Pitfall: Forget export * as schema and you'll lose types.

Implementing Advanced CRUD Operations

Now for queries: selects with joins, batch inserts, conditional updates, and cascading deletes. Drizzle shines with SQL-like composability without boilerplate.

Full CRUD Example on Users and Posts

src/crud.ts
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);

Full-stack CRUD: batch inserts, joins via leftJoin (use relations for nested selects), composable wheres (and, ilike). Inferred types: userPosts is {userName: string, ...}. Pitfall: enumValues[1] for runtime values; use eq(posts.status, 'PUBLISHED') directly instead.

Adding Comments Table with Relations

src/schema-comments.ts
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),
}));

Schema extension: comments with bidirectional foreign keys and onDelete: 'cascade'. Chained relations for deep queries. Regenerate migrations after adding. Pitfall: references must match exactly, or you'll get integrity errors.

Advanced Queries with Nested Relations and Transactions

src/advanced.ts
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));

Recursive relational queries in 1 database roundtrip via query.posts.findFirst({ with }). Explicit transactions for consistency (auto-rollback on errors). Performance: 90% fewer queries vs manual loops. Pitfall: Use returning() to get post-insert IDs in transactions.

Best Practices

  • Environment variables for DB URL: Always use process.env.DATABASE_URL with pooling (pg.Pool).
  • Explicit indexes: Add index('idx_posts_author') on foreign keys for fast queries.
  • Pagination: Use limit(10).offset(page * 10) + separate count().
  • Upserts: insert().values().onConflictDoUpdate() for idempotency.
  • Prepared statements: Drizzle handles them natively; avoid raw SQL except for analytics.

Common Errors to Avoid

  • Circular relations: Use eq(table.col, other.col) in manual joins if relations() loops.
  • Manual migrations: Never edit generated SQL; use push in dev, migrate in prod.
  • Pool exhaustion: Limit Pool(max=20) and close in serverless (Next.js).
  • String enums: pgEnum stores strings; mismatches cause enumValues issues.

Next Steps