Skip to content
FLAVIO COPES
flaviocopes.com

Drizzle ORM: an introduction

By

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, 'flavio@flaviocopes.com'))

Insert a row:

await db.insert(users).values({
  name: 'Flavio',
  email: 'flavio@flaviocopes.com',
})

Update a row:

await db
  .update(users)
  .set({ name: 'Flavio Copes' })
  .where(eq(users.email, 'flavio@flaviocopes.com'))

Delete a row:

await db
  .delete(users)
  .where(eq(users.email, 'flavio@flaviocopes.com'))

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.

Tagged: Database · All topics
~~~

Related posts about database: