Back to Blog
DrizzleORMTypeScriptDatabase

Getting Started with Drizzle ORM

Learn Drizzle ORM for TypeScript. From schema definition to queries to migrations.

B
Bootspring Team
Engineering
June 13, 2021
6 min read

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 PostgreSQL
1// 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.

Share this article

Help spread the word about Bootspring