Using the Drizzle Client
Learn how to use the Drizzle client for queries, inserts, updates, transactions, and relational queries in the Next.js Drizzle SaaS Kit.
The Drizzle client provides two APIs for querying data: the Select API (SQL-like) and the Query API (relational). Both are fully type-safe, meaning TypeScript catches errors at compile time rather than runtime.
Importing
Import the client and schema from @kit/database:
import { db, user, organization, member } from '@kit/database';The db object is the Drizzle client. The table exports (user, organization, etc.) are used in queries to reference columns and build conditions.
Select API (SQL-like)
The Select API maps closely to SQL. If you know SQL, this feels familiar. The queries you write look like the SQL they generate.
Basic Select
import { eq, and, desc, like } from 'drizzle-orm';import { db, user } from '@kit/database';// Select all usersconst allUsers = await db.select().from(user);// Select with filterconst activeUsers = await db .select() .from(user) .where(eq(user.banned, false));// Select specific columnsconst userEmails = await db .select({ id: user.id, email: user.email }) .from(user);// Order and limitconst recentUsers = await db .select() .from(user) .orderBy(desc(user.createdAt)) .limit(10);Filters
import { eq, ne, gt, gte, lt, lte, like, ilike, and, or, isNull, inArray } from 'drizzle-orm';// Equality.where(eq(user.email, 'test@example.com'))// Not equal.where(ne(user.role, 'admin'))// Comparisons.where(gt(user.createdAt, new Date('2024-01-01')))// Pattern matching (case-insensitive).where(ilike(user.name, '%john%'))// Null checks.where(isNull(user.image))// In array.where(inArray(user.role, ['admin', 'owner']))// Multiple conditions (AND).where(and( eq(user.banned, false), isNull(user.banExpires)))// Multiple conditions (OR).where(or( eq(user.role, 'admin'), eq(user.role, 'owner')))Joins
import { eq } from 'drizzle-orm';import { db, user, member, organization } from '@kit/database';// Inner joinconst memberships = await db .select({ userName: user.name, orgName: organization.name, role: member.role, }) .from(member) .innerJoin(user, eq(member.userId, user.id)) .innerJoin(organization, eq(member.organizationId, organization.id));// Left join (nullable results)const usersWithOrgs = await db .select() .from(user) .leftJoin(member, eq(user.id, member.userId));Insert
import { db, user, organization } from '@kit/database';// Single insert with returningconst [newUser] = await db .insert(user) .values({ id: crypto.randomUUID(), name: 'John Doe', email: 'john@example.com', emailVerified: false, }) .returning();// Insert with returning specific columnsconst [{ id }] = await db .insert(organization) .values({ id: crypto.randomUUID(), name: 'Acme Corp', slug: 'acme-corp', createdAt: new Date(), }) .returning({ id: organization.id });// Bulk insertawait db.insert(member).values([ { id: crypto.randomUUID(), userId: 'user1', organizationId: 'org1', role: 'owner', createdAt: new Date() }, { id: crypto.randomUUID(), userId: 'user2', organizationId: 'org1', role: 'member', createdAt: new Date() },]);Update
import { eq } from 'drizzle-orm';import { db, user } from '@kit/database';// Update with filterawait db .update(user) .set({ name: 'Jane Doe' }) .where(eq(user.id, userId));// Update with returningconst [updated] = await db .update(user) .set({ emailVerified: true }) .where(eq(user.id, userId)) .returning();Updates without a where() clause update all rows in the table. Drizzle doesn't warn you about this at compile time.
Delete
import { eq, lt } from 'drizzle-orm';import { db, session } from '@kit/database';// Delete by IDawait db.delete(session).where(eq(session.id, sessionId));// Delete expired sessionsawait db.delete(session).where(lt(session.expiresAt, new Date()));Upsert (Insert or Update)
import { db, subscription } from '@kit/database';await db .insert(subscription) .values({ id: subscriptionId, plan: 'pro', referenceId: organizationId, status: 'active', }) .onConflictDoUpdate({ target: subscription.id, set: { plan: 'pro', status: 'active', }, });Query API (Relational)
The Query API provides a higher-level interface for fetching related data without writing joins. It requires relations to be defined in your schema (the kit's core.ts includes these).
Basic Query
import { eq } from 'drizzle-orm';import { db } from '@kit/database';// Find oneconst foundUser = await db.query.user.findFirst({ where: (user, { eq }) => eq(user.email, 'test@example.com'),});// Find manyconst allOrgs = await db.query.organization.findMany({ limit: 10, orderBy: (org, { desc }) => desc(org.createdAt),});With Relations
// User with their organization membershipsconst userWithMemberships = await db.query.user.findFirst({ where: (user, { eq }) => eq(user.id, userId), with: { members: { with: { organization: true, }, }, },});// Organization with members and their user profilesconst orgWithMembers = await db.query.organization.findFirst({ where: (org, { eq }) => eq(org.id, orgId), with: { members: { with: { user: { columns: { id: true, name: true, email: true, image: true, }, }, }, }, },});Selecting Columns
const users = await db.query.user.findMany({ columns: { id: true, name: true, email: true, // Omit sensitive fields like password hash },});Filtering Relations
const orgWithActiveMembers = await db.query.organization.findFirst({ where: (org, { eq }) => eq(org.id, orgId), with: { members: { where: (member, { eq }) => eq(member.role, 'admin'), orderBy: (member, { desc }) => desc(member.createdAt), limit: 5, }, },});Transactions
Use transactions for operations that must succeed or fail together:
import { db, user, organization, member } from '@kit/database';const result = await db.transaction(async (tx) => { // Create organization const [org] = await tx .insert(organization) .values({ id: crypto.randomUUID(), name: 'New Org', slug: 'new-org', createdAt: new Date(), }) .returning(); // Add creator as owner await tx.insert(member).values({ id: crypto.randomUUID(), organizationId: org.id, userId: userId, role: 'owner', createdAt: new Date(), }); return org;});If any operation in the transaction fails, all changes are rolled back.
Nested Transactions (Savepoints)
await db.transaction(async (tx) => { await tx.insert(user).values({ /* ... */ }); try { await tx.transaction(async (nestedTx) => { await nestedTx.insert(member).values({ /* ... */ }); throw new Error('Something went wrong'); }); } catch (e) { // Nested transaction rolled back, outer continues console.log('Nested operation failed, continuing...'); } // This still commits await tx.insert(organization).values({ /* ... */ });});Auth Context Pattern
Use auth context utilities for multi-tenant queries. This prevents accidentally querying data from other organizations:
import { createOrgAuthContext } from '@kit/database';import { db, projects } from '@kit/database';async function getProjects(userId: string, organizationId: string, role: string) { const ctx = createOrgAuthContext(userId, organizationId, role); // ctx.org() generates eq(projects.organizationId, organizationId) const orgProjects = await db .select() .from(projects) .where(ctx.org(projects)); return orgProjects;}async function createProject( userId: string, organizationId: string, role: string, data: { name: string },) { const ctx = createOrgAuthContext(userId, organizationId, role); // ctx.values() automatically adds organizationId to the insert const [project] = await db .insert(projects) .values(ctx.values(projects, { id: crypto.randomUUID(), name: data.name, createdAt: new Date(), })) .returning(); return project;}Common Patterns
Pagination
async function getPaginatedUsers(page: number, pageSize: number = 20) { const offset = page * pageSize; const users = await db .select() .from(user) .orderBy(desc(user.createdAt)) .limit(pageSize) .offset(offset); return users;}Cursor-based Pagination
async function getUsersAfter(cursor: Date, limit: number = 20) { return db .select() .from(user) .where(lt(user.createdAt, cursor)) .orderBy(desc(user.createdAt)) .limit(limit);}Counting
import { count } from 'drizzle-orm';const [{ total }] = await db .select({ total: count() }) .from(user) .where(eq(user.banned, false));Aggregations
import { count, sum, avg } from 'drizzle-orm';const stats = await db .select({ totalMembers: count(), }) .from(member) .where(eq(member.organizationId, orgId));Common Mistakes to Avoid
Forgetting organizationId filter: Every query on tenant data must include an organization filter. Use createOrgAuthContext() to make this automatic and prevent cross-tenant data leaks.
N+1 queries in loops: Don't fetch related data in a loop. Use the Query API with with to load relations in a single query:
// Bad: N+1 queriesconst users = await db.select().from(user);for (const u of users) { const members = await db.select().from(member).where(eq(member.userId, u.id));}// Good: Single query with relationsconst users = await db.query.user.findMany({ with: { members: true },});Update/delete without WHERE: Always include a where() clause. Drizzle doesn't prevent you from updating or deleting all rows.
Using db instead of tx in transactions: When inside a transaction callback, always use the tx parameter, not the global db. Using db bypasses the transaction.
Performance Tips
- Select only needed columns to reduce data transfer
- Add indexes on columns used in
WHERE,JOIN,ORDER BY - Use
limitfor large result sets - Avoid N+1 queries by using
withfor relations instead of loops - Use transactions for multiple related writes
For the full Drizzle ORM documentation, see orm.drizzle.team.
Frequently Asked Questions
When should I use the Select API vs the Query API?
How do I avoid N+1 queries?
Do transactions work with serverless deployments?
How do I handle optimistic locking?
Why does my Query API call return undefined?
Next: Migrating to SQLite