Drizzle ORM provides type-safe database access with SQL-like syntax. Here's how to use it.
Setup#
npm install drizzle-orm
npm install drizzle-kit -D
npm install @libsql/client # or pg for PostgreSQL1// drizzle.config.ts
2import { defineConfig } from 'drizzle-kit';
3
4export default defineConfig({
5 schema: './src/db/schema.ts',
6 out: './drizzle',
7 dialect: 'postgresql',
8 dbCredentials: {
9 url: process.env.DATABASE_URL!,
10 },
11});Schema Definition#
1// src/db/schema.ts
2import {
3 pgTable,
4 serial,
5 text,
6 varchar,
7 timestamp,
8 boolean,
9 integer,
10 primaryKey,
11 uniqueIndex,
12} from 'drizzle-orm/pg-core';
13import { relations } from 'drizzle-orm';
14
15export const users = pgTable('users', {
16 id: serial('id').primaryKey(),
17 email: varchar('email', { length: 255 }).notNull().unique(),
18 name: text('name').notNull(),
19 password: text('password').notNull(),
20 role: varchar('role', { length: 50 }).default('user'),
21 isActive: boolean('is_active').default(true),
22 createdAt: timestamp('created_at').defaultNow(),
23 updatedAt: timestamp('updated_at').defaultNow(),
24}, (table) => ({
25 emailIdx: uniqueIndex('email_idx').on(table.email),
26}));
27
28export const posts = pgTable('posts', {
29 id: serial('id').primaryKey(),
30 title: varchar('title', { length: 255 }).notNull(),
31 content: text('content'),
32 published: boolean('published').default(false),
33 authorId: integer('author_id').references(() => users.id),
34 createdAt: timestamp('created_at').defaultNow(),
35 updatedAt: timestamp('updated_at').defaultNow(),
36});
37
38export const tags = pgTable('tags', {
39 id: serial('id').primaryKey(),
40 name: varchar('name', { length: 50 }).notNull().unique(),
41});
42
43export const postTags = pgTable('post_tags', {
44 postId: integer('post_id').references(() => posts.id),
45 tagId: integer('tag_id').references(() => tags.id),
46}, (table) => ({
47 pk: primaryKey({ columns: [table.postId, table.tagId] }),
48}));
49
50// Relations
51export const usersRelations = relations(users, ({ many }) => ({
52 posts: many(posts),
53}));
54
55export const postsRelations = relations(posts, ({ one, many }) => ({
56 author: one(users, {
57 fields: [posts.authorId],
58 references: [users.id],
59 }),
60 postTags: many(postTags),
61}));
62
63export const postTagsRelations = relations(postTags, ({ one }) => ({
64 post: one(posts, {
65 fields: [postTags.postId],
66 references: [posts.id],
67 }),
68 tag: one(tags, {
69 fields: [postTags.tagId],
70 references: [tags.id],
71 }),
72}));Database Connection#
1// src/db/index.ts
2import { drizzle } from 'drizzle-orm/node-postgres';
3import { Pool } from 'pg';
4import * as schema from './schema';
5
6const pool = new Pool({
7 connectionString: process.env.DATABASE_URL,
8});
9
10export const db = drizzle(pool, { schema });
11
12// For Turso/LibSQL
13import { drizzle } from 'drizzle-orm/libsql';
14import { createClient } from '@libsql/client';
15
16const client = createClient({
17 url: process.env.DATABASE_URL!,
18 authToken: process.env.DATABASE_AUTH_TOKEN,
19});
20
21export const db = drizzle(client, { schema });Basic Queries#
1import { eq, and, or, gt, lt, like, desc, asc, sql } from 'drizzle-orm';
2import { db } from './db';
3import { users, posts } from './db/schema';
4
5// Select all
6const allUsers = await db.select().from(users);
7
8// Select specific columns
9const userEmails = await db
10 .select({ id: users.id, email: users.email })
11 .from(users);
12
13// Where clause
14const activeUsers = await db
15 .select()
16 .from(users)
17 .where(eq(users.isActive, true));
18
19// Multiple conditions
20const filteredUsers = await db
21 .select()
22 .from(users)
23 .where(
24 and(
25 eq(users.role, 'admin'),
26 gt(users.createdAt, new Date('2024-01-01'))
27 )
28 );
29
30// OR conditions
31const searchUsers = await db
32 .select()
33 .from(users)
34 .where(
35 or(
36 like(users.email, '%@gmail.com'),
37 like(users.email, '%@outlook.com')
38 )
39 );
40
41// Order and limit
42const recentUsers = await db
43 .select()
44 .from(users)
45 .orderBy(desc(users.createdAt))
46 .limit(10)
47 .offset(0);
48
49// Find one
50const user = await db
51 .select()
52 .from(users)
53 .where(eq(users.id, 1))
54 .limit(1);Insert Operations#
1// Single insert
2const newUser = await db
3 .insert(users)
4 .values({
5 email: 'john@example.com',
6 name: 'John Doe',
7 password: hashedPassword,
8 })
9 .returning();
10
11// Multiple insert
12const newUsers = await db
13 .insert(users)
14 .values([
15 { email: 'user1@example.com', name: 'User 1', password: 'hash1' },
16 { email: 'user2@example.com', name: 'User 2', password: 'hash2' },
17 ])
18 .returning();
19
20// On conflict (upsert)
21const upserted = await db
22 .insert(users)
23 .values({
24 email: 'john@example.com',
25 name: 'John Doe',
26 password: hashedPassword,
27 })
28 .onConflictDoUpdate({
29 target: users.email,
30 set: {
31 name: 'John Doe Updated',
32 updatedAt: new Date(),
33 },
34 })
35 .returning();
36
37// On conflict do nothing
38await db
39 .insert(users)
40 .values({ email: 'existing@example.com', name: 'Test', password: 'hash' })
41 .onConflictDoNothing();Update Operations#
1// Basic update
2await db
3 .update(users)
4 .set({ name: 'Jane Doe' })
5 .where(eq(users.id, 1));
6
7// Update with returning
8const updated = await db
9 .update(users)
10 .set({
11 isActive: false,
12 updatedAt: new Date(),
13 })
14 .where(eq(users.email, 'john@example.com'))
15 .returning();
16
17// Conditional update
18await db
19 .update(posts)
20 .set({ published: true })
21 .where(
22 and(
23 eq(posts.authorId, userId),
24 eq(posts.published, false)
25 )
26 );
27
28// Increment
29await db
30 .update(posts)
31 .set({
32 views: sql`${posts.views} + 1`,
33 })
34 .where(eq(posts.id, postId));Delete Operations#
1// Basic delete
2await db.delete(users).where(eq(users.id, 1));
3
4// Delete with returning
5const deleted = await db
6 .delete(users)
7 .where(eq(users.isActive, false))
8 .returning();
9
10// Delete all (be careful!)
11await db.delete(users);Joins and Relations#
1// Inner join
2const postsWithAuthors = await db
3 .select({
4 post: posts,
5 author: users,
6 })
7 .from(posts)
8 .innerJoin(users, eq(posts.authorId, users.id));
9
10// Left join
11const usersWithPosts = await db
12 .select({
13 user: users,
14 post: posts,
15 })
16 .from(users)
17 .leftJoin(posts, eq(users.id, posts.authorId));
18
19// Using relations (query API)
20const usersWithRelations = await db.query.users.findMany({
21 with: {
22 posts: true,
23 },
24});
25
26// Nested relations
27const postsWithAll = await db.query.posts.findMany({
28 with: {
29 author: true,
30 postTags: {
31 with: {
32 tag: true,
33 },
34 },
35 },
36 where: eq(posts.published, true),
37 orderBy: desc(posts.createdAt),
38 limit: 10,
39});
40
41// Select specific columns in relations
42const usersWithPostTitles = await db.query.users.findMany({
43 columns: {
44 id: true,
45 name: true,
46 },
47 with: {
48 posts: {
49 columns: {
50 title: true,
51 published: true,
52 },
53 },
54 },
55});Transactions#
1// Basic transaction
2await db.transaction(async (tx) => {
3 const user = await tx
4 .insert(users)
5 .values({ email: 'new@example.com', name: 'New', password: 'hash' })
6 .returning();
7
8 await tx.insert(posts).values({
9 title: 'First Post',
10 content: 'Hello!',
11 authorId: user[0].id,
12 });
13});
14
15// With rollback
16await db.transaction(async (tx) => {
17 try {
18 await tx.insert(users).values({ ... });
19 await tx.insert(posts).values({ ... });
20
21 if (someCondition) {
22 tx.rollback();
23 }
24 } catch (error) {
25 // Transaction automatically rolled back
26 throw error;
27 }
28});Migrations#
1# Generate migration
2npx drizzle-kit generate
3
4# Apply migrations
5npx drizzle-kit migrate
6
7# Push schema (development)
8npx drizzle-kit push
9
10# Open Drizzle Studio
11npx drizzle-kit studio// Custom migration
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { db } from './db';
await migrate(db, { migrationsFolder: './drizzle' });Best Practices#
Schema:
✓ Use proper column types
✓ Define relations explicitly
✓ Add indexes for queried columns
✓ Use timestamps for audit
Queries:
✓ Use prepared statements
✓ Select only needed columns
✓ Use transactions for multi-ops
✓ Handle errors appropriately
Performance:
✓ Add indexes
✓ Use connection pooling
✓ Batch inserts
✓ Profile slow queries
Conclusion#
Drizzle ORM provides type-safe database access with a SQL-like API. Its schema-first approach with TypeScript gives excellent DX while maintaining full control over queries. Use relations for complex queries and transactions for data consistency.