drizzle orm
TypeScript ORM with SQL-like queries
$ npx docs2skills add drizzle-ormDrizzle ORM
TypeScript ORM with SQL-like queries and zero dependencies
What this skill does
Drizzle ORM is a headless TypeScript ORM that embraces SQL rather than abstracting away from it. Unlike traditional ORMs that force you to build projects around them, Drizzle lets you build projects with it - providing both SQL-like and relational query APIs for maximum flexibility.
The core philosophy is "if you know SQL, you know Drizzle." It offers type-safe database operations, automatic migrations, and serverless-ready performance with exactly zero dependencies. Drizzle supports PostgreSQL, MySQL, SQLite, and other databases through their native drivers, making it ideal for modern applications that need performant, type-safe data access without framework lock-in.
Prerequisites
- Node.js 14+ or compatible runtime (Bun, Deno, Cloudflare Workers)
- TypeScript 4.5+
- Database driver for your chosen database:
- PostgreSQL:
pg,postgres,@neondatabase/serverless - MySQL:
mysql2,@planetscale/database - SQLite:
better-sqlite3,@libsql/client,@cloudflare/d1
- PostgreSQL:
Quick start
npm install drizzle-orm
npm install -D drizzle-kit
# Install your database driver, e.g.:
npm install pg @types/pg
// schema.ts
import { pgTable, serial, varchar, integer } from 'drizzle-orm/pg-core';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 256 }),
email: varchar('email', { length: 256 }),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 256 }),
userId: integer('user_id').references(() => users.id),
});
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool);
// Query examples
const allUsers = await db.select().from(users);
const userWithPosts = await db.query.users.findFirst({
with: { posts: true }
});
Core concepts
Schema Definition: Define tables using type-safe column builders (serial, varchar, integer, etc.) that map directly to SQL DDL.
Dual Query APIs:
- SQL-like API mirrors SQL syntax closely (
select().from().where()) - Relational API for nested data fetching (
query.users.findMany({ with: { posts: true } }))
Dialect-Specific: Separate entry points for each database (drizzle-orm/pg-core, drizzle-orm/mysql-core, drizzle-orm/sqlite-core) ensure optimal SQL generation.
Migration System: Drizzle Kit generates and manages migrations by comparing schema changes, no manual migration writing required.
Driver Agnostic: Works with any database driver - you choose your connection layer, Drizzle handles the queries.
Key API surface
// Schema definition
import { pgTable, serial, varchar, timestamp, relations } from 'drizzle-orm/pg-core';
// Query building
import { eq, and, or, like, gt, lt, isNull } from 'drizzle-orm';
// Database instance
import { drizzle } from 'drizzle-orm/node-postgres';
// Core query methods
db.select().from(table).where(condition)
db.insert(table).values(data).returning()
db.update(table).set(data).where(condition)
db.delete(table).where(condition)
// Relational queries
db.query.tableName.findMany({ with: { relations: true } })
db.query.tableName.findFirst({ where: condition })
// Transactions
db.transaction(async (tx) => { /* queries */ })
// Raw SQL
import { sql } from 'drizzle-orm';
db.execute(sql`SELECT * FROM users WHERE id = ${userId}`)
// Batch operations
db.batch([query1, query2, query3])
Common patterns
Basic CRUD operations:
// Insert
const newUser = await db.insert(users)
.values({ name: 'John', email: 'john@example.com' })
.returning();
// Select with conditions
const activeUsers = await db.select()
.from(users)
.where(and(eq(users.active, true), gt(users.createdAt, new Date('2024-01-01'))));
// Update
await db.update(users)
.set({ lastLogin: new Date() })
.where(eq(users.id, userId));
Joins and relationships:
// SQL-like joins
const usersWithPosts = await db.select()
.from(users)
.leftJoin(posts, eq(posts.userId, users.id))
.where(eq(users.active, true));
// Relational queries (single SQL query)
const userWithData = await db.query.users.findMany({
with: {
posts: {
with: { comments: true }
},
profile: true
}
});
Dynamic query building:
let query = db.select().from(users);
if (nameFilter) {
query = query.where(like(users.name, `%${nameFilter}%`));
}
if (sortBy === 'name') {
query = query.orderBy(users.name);
}
const results = await query;
Transactions with error handling:
await db.transaction(async (tx) => {
const user = await tx.insert(users).values(userData).returning();
await tx.insert(profiles).values({ userId: user[0].id, ...profileData });
// Transaction automatically rolls back on error
});
Prepared statements for performance:
const getUserById = db.select()
.from(users)
.where(eq(users.id, $id))
.prepare();
// Reuse prepared statement
const user1 = await getUserById.execute({ id: 1 });
const user2 = await getUserById.execute({ id: 2 });
Configuration
Drizzle Kit config (drizzle.config.ts):
import type { Config } from 'drizzle-kit';
export default {
schema: './src/schema.ts',
out: './migrations',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
verbose: true,
strict: true,
} satisfies Config;
Environment variables:
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
Database connection options:
// Connection pooling
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
});
// Custom logger
const db = drizzle(client, {
schema,
logger: true // or custom logger function
});
Best practices
- Use prepared statements for frequently executed queries to improve performance
- Leverage TypeScript strict mode - Drizzle's type safety shines with strict TypeScript
- Define relations in schema for type-safe relational queries, even if you don't use them
- Use transactions for multi-table operations to ensure data consistency
- Prefer relational queries over joins when fetching nested data - they're more efficient
- Use
returning()on inserts/updates when you need the affected rows back - Batch operations when possible using
db.batch()for better performance - Use specific database drivers rather than generic ones for better performance and features
- Keep schema files separate from business logic for better organization
- Use Drizzle Studio for database inspection and debugging during development
Gotchas and common mistakes
Schema definition pitfalls:
- Column constraints must be defined in schema, not enforced by Drizzle at runtime
- Changing column names requires manual migration handling for data preservation
serial()andinteger().primaryKey()are different - serial auto-increments- Foreign key references require functions:
.references(() => users.id), not.references(users.id)
Query API confusion:
- Relational queries (
db.query.table) and SQL-like queries (db.select()) have different syntax - Relational queries generate exactly one SQL query, not N+1 queries
findFirst()returns undefined if no results,findMany()returns empty array- You cannot mix relational and SQL-like syntax in the same query
Type safety gotchas:
- TypeScript inference works best with
constassertions on filter conditions - Dynamic query building can lose type information - use proper typing
sqltemplate literals bypass type checking - use sparingly and carefully- Prepared statements require explicit typing for parameters
Migration and schema issues:
- Schema changes require running
drizzle-kit generatebefore deployment - Drizzle Kit doesn't handle data transformations - write custom migrations for complex changes
- Schema file location must match
drizzle.config.tsconfiguration - Breaking changes in schema can make existing migrations incompatible
Performance traps:
- Not using indexes on frequently queried columns
- Using relational queries when you only need specific columns (use
columnsoption) - Forgetting to use connection pooling in serverless environments
- Not utilizing prepared statements for repeated queries
Connection and driver issues:
- Database connection strings must match driver requirements exactly
- Some drivers require specific configuration for serverless environments
- Connection pooling behaves differently across drivers
- Edge runtime compatibility varies by database driver choice