Drizzle ORM: an introduction
By Flavio Copes
Drizzle ORM is a TypeScript-first SQL toolkit. Learn schema definitions, queries, relations, and migrations with drizzle-kit for Node, Bun, and Cloudflare D1.
Most apps need a database. You want type safety, but you don’t want to fight your ORM every time you write a join.
Drizzle ORM is what I reach for. It’s a TypeScript library that stays close to SQL. If you know SQL, you know Drizzle.
Prisma takes a different path. You write a schema in its own DSL, run codegen, and query through a generated client. Drizzle is just TypeScript. Your tables are TypeScript objects. Your queries look like SQL. No codegen step for the client.
Install it with:
npm i drizzle-orm
Defining a schema
You describe tables in a schema.ts file. Each table is a function call with column definitions.
For SQLite or Cloudflare D1, use sqliteTable:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
email: text('email').notNull(),
})
On Postgres, swap sqliteTable for pgTable and import from drizzle-orm/pg-core. The shape is the same.
Querying
You create a db instance by passing your database driver to drizzle(). On Node with Postgres, that looks like drizzle(pool). On D1, you pass the binding: drizzle(env.DB).
Then you query with a builder that mirrors SQL. select(), from(), where() — same words you’d use in a SQL statement.
Select every row:
const allUsers = await db.select().from(users)
Filter with where() and eq():
import { eq } from 'drizzle-orm'
const flavio = await db
.select()
.from(users)
.where(eq(users.email, '[email protected]'))
Insert a row:
await db.insert(users).values({
name: 'Flavio',
email: '[email protected]',
})
Update a row:
await db
.update(users)
.set({ name: 'Flavio Copes' })
.where(eq(users.email, '[email protected]'))
Delete a row:
await db
.delete(users)
.where(eq(users.email, '[email protected]'))
Every method returns typed results. Change a column name in the schema and TypeScript catches the broken query at compile time.
Relational queries
Manual joins work, but Drizzle has a nicer API for related data. Define the relationship once:
import { relations } from 'drizzle-orm'
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
userId: integer('user_id').notNull().references(() => users.id),
})
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}))
Pass the schema when you create db. Then fetch users with their posts in one call:
const usersWithPosts = await db.query.users.findMany({
with: {
posts: true,
},
})
No hand-written JOIN. Drizzle figures out the SQL for you.
Migrations with drizzle-kit
Schema changes need migrations. drizzle-kit is the CLI that handles them.
Install it:
npm i -D drizzle-kit
Add a config file:
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'sqlite',
})
When you change the schema, generate a SQL migration:
npx drizzle-kit generate
This writes a .sql file you can read and commit. You see exactly what will run against the database. No surprises.
Apply it to the database:
npx drizzle-kit migrate
For quick prototyping, skip the migration files and push the schema directly:
npx drizzle-kit push
Want to browse your data in a GUI? Run Drizzle Studio:
npx drizzle-kit studio
It opens a local UI where you can inspect tables and run queries.
Where it runs
Drizzle works anywhere TypeScript runs. Node, Bun, Deno, serverless, edge. It supports Postgres, MySQL, SQLite, and a growing list of hosted databases.
I use it with Cloudflare D1 on Workers. D1 is SQLite at the edge, and Drizzle has a first-class driver for it. The setup is one import:
import { drizzle } from 'drizzle-orm/d1'
const db = drizzle(env.DB, { schema })
If you want the full Astro + D1 + Drizzle stack, I wrote a separate guide: Astro with D1 and Drizzle.
Why Drizzle
My advice: pick an ORM that respects SQL, not one that hides it.
Drizzle queries read like SQL. Your existing database knowledge transfers directly. There’s no magic layer between you and the database.
That’s why it’s my default for new TypeScript projects.
Related posts about database: