Drizzle ORM Reference: Schema, Queries, Relational API, Migrations & Edge Runtimes
Drizzle ORM is TypeScript-first, SQL-first, and designed to stay close to SQL instead of abstracting it away. You write queries that map 1:1 to SQL — no magic, no proxy objects, no surprises. The key difference from Prisma: Drizzle is lighter (no Rust binary, no DMMF), runs in edge environments (Cloudflare Workers, Deno), and lets you see the SQL it generates. The trade-off: slightly more verbose schema definitions.
1. Schema Definition & Types
Define tables, columns, constraints, and infer TypeScript types from schema
import { pgTable, serial, text, varchar, integer, boolean,
timestamp, uuid, index, uniqueIndex } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
// Define tables in schema.ts:
export const users = pgTable("users", {
id: serial("id").primaryKey(),
name: varchar("name", { length: 100 }).notNull(),
email: text("email").notNull().unique(),
age: integer("age"),
verified: boolean("verified").notNull().default(false),
createdAt: timestamp("created_at").defaultNow().notNull(),
uuid: uuid("uuid").default(sql`gen_random_uuid()`),
}, (table) => ({
emailIdx: uniqueIndex("users_email_idx").on(table.email),
nameIdx: index("users_name_idx").on(table.name),
}));
export const posts = pgTable("posts", {
id: serial("id").primaryKey(),
title: text("title").notNull(),
content: text("content"),
authorId: integer("author_id").notNull().references(() => users.id, { onDelete: "cascade" }),
published: boolean("published").default(false).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// Infer TypeScript types from schema (no separate type definitions):
import type { InferSelectModel, InferInsertModel } from "drizzle-orm";
type User = InferSelectModel<typeof users>; // row shape for SELECT
type NewUser = InferInsertModel<typeof users>; // shape for INSERT (id optional)
type UpdateUser = Partial<Omit<NewUser, "id">>; // shape for UPDATE
2. Queries — Select, Insert, Update, Delete
Type-safe queries, where clauses, joins, and returning
import { drizzle } from "drizzle-orm/node-postgres";
import { eq, and, or, like, gte, lte, isNull, desc, asc, count } from "drizzle-orm";
import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool, { schema: { users, posts } }); // schema enables relational API
// SELECT:
const allUsers = await db.select().from(users);
const alice = await db.select().from(users).where(eq(users.email, "alice@example.com")).limit(1);
// Select specific columns:
const names = await db.select({ id: users.id, name: users.name }).from(users);
// WHERE with multiple conditions:
const results = await db.select().from(users).where(
and(
eq(users.verified, true),
gte(users.age, 18),
like(users.name, "%Alice%"),
)
).orderBy(desc(users.createdAt)).limit(20).offset(40);
// JOIN:
const postsWithAuthors = await db
.select({ post: posts, author: { name: users.name, email: users.email } })
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true));
// INSERT:
const [newUser] = await db.insert(users)
.values({ name: "Alice", email: "alice@example.com", verified: true })
.returning(); // returns the inserted row with generated id
// Bulk insert:
await db.insert(users).values([
{ name: "Bob", email: "bob@example.com" },
{ name: "Carol", email: "carol@example.com" },
]);
// UPDATE:
const [updated] = await db.update(users)
.set({ name: "Alice Smith", verified: true })
.where(eq(users.id, 1))
.returning();
// DELETE:
await db.delete(users).where(eq(users.id, 1));
// COUNT:
const [{ total }] = await db.select({ total: count() }).from(users);
3. Relational Queries & Transactions
db.query relational API (with), nested relations, and transactions
import { relations } from "drizzle-orm";
// Define relations (for the relational API only — not required for joins):
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));
// Relational API — nested queries (uses JOINs under the hood):
const usersWithPosts = await db.query.users.findMany({
with: {
posts: {
where: eq(posts.published, true),
orderBy: desc(posts.createdAt),
limit: 5,
columns: { title: true, createdAt: true }, // select specific columns
},
},
where: eq(users.verified, true),
orderBy: asc(users.name),
limit: 10,
});
// usersWithPosts[0].posts is typed as array of { title: string, createdAt: Date }
// Find one (returns undefined if not found):
const user = await db.query.users.findFirst({
where: eq(users.email, "alice@example.com"),
with: { posts: true },
});
// Transactions (all queries share the same connection):
const result = await db.transaction(async (tx) => {
const [user] = await tx.insert(users)
.values({ name: "Alice", email: "alice@example.com" })
.returning();
await tx.insert(posts)
.values({ title: "First Post", authorId: user.id });
return user;
});
// If any query throws, the transaction is automatically rolled back
4. Migrations with drizzle-kit
Generate and run migrations, drizzle.config.ts, and push for development
// npm install -D drizzle-kit
// drizzle.config.ts:
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle", // migrations output directory
dialect: "postgresql",
dbCredentials: { url: process.env.DATABASE_URL! },
});
// Generate migration from schema changes:
// npx drizzle-kit generate
// Creates: drizzle/0001_add_users_table.sql + drizzle/meta/
// Apply migrations (programmatic — run at app startup or in CI):
import { migrate } from "drizzle-orm/node-postgres/migrator";
await migrate(db, { migrationsFolder: "./drizzle" });
// drizzle-kit push (dev only — bypasses migration files, syncs schema directly):
// npx drizzle-kit push
// Use for: rapid prototyping, local dev. Never in production.
// drizzle-kit studio (visual query browser):
// npx drizzle-kit studio
// Common commands:
// npx drizzle-kit generate — generate SQL migration from schema diff
// npx drizzle-kit migrate — apply pending migrations
// npx drizzle-kit push — sync schema without migration files (dev only)
// npx drizzle-kit introspect — generate schema from existing database
5. Edge Runtimes — Turso, Neon Serverless & D1
Drizzle with Turso (libSQL), Neon serverless, and Cloudflare D1
// Turso (LibSQL) — SQLite at the edge:
import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN,
});
const db = drizzle(client, { schema });
// Neon serverless (HTTP-based, works in edge functions):
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
const sql_client = neon(process.env.DATABASE_URL!);
const db = drizzle(sql_client, { schema });
// Neon serverless transactions require pool (not HTTP client):
import { drizzle } from "drizzle-orm/neon-serverless";
import { Pool } from "@neondatabase/serverless";
// Cloudflare D1 (SQLite in Workers):
// wrangler.toml: [[d1_databases]] binding = "DB"
import { drizzle } from "drizzle-orm/d1";
export default {
async fetch(request: Request, env: Env) {
const db = drizzle(env.DB, { schema });
const users = await db.select().from(usersTable).all();
return Response.json(users);
},
};
// SQLite (local / tests):
import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
const sqlite = new Database(":memory:"); // in-memory for tests
const db = drizzle(sqlite, { schema });
Track Node.js and TypeScript releases at ReleaseRun. Related: Prisma Reference | SQLAlchemy 2.0 Reference | TypeScript Reference | TypeScript EOL Tracker
Founded
2023 in London, UK
Contact
hello@releaserun.com