Pick up Drizzle for the first time after a few years of Prisma and the surprise is how little is hidden. There is no separate schema language. There is no generated client. Your queries look like SQL written in TypeScript. The library is roughly a thin layer that turns typed builder calls into prepared statements and gives you back rows with the right shape.
That sounds underwhelming, and that is the point. The teams I have seen migrate to Drizzle did it because they were tired of the Prisma engine being in their stack trace, tired of guessing what the engine was doing, or trying to deploy to an edge runtime where a Rust binary was not welcome.
Drizzle is not "Prisma but lighter." It is a different bet about what an ORM should give you in 2025: types and migrations, yes; opinions and abstraction, no.
Schema Lives In Your TypeScript
The schema is plain TypeScript files. No DSL, no generation step you have to remember.
// schema.ts
import { pgTable, text, timestamp, uuid, integer } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
authorId: uuid('author_id').notNull().references(() => users.id),
title: text('title').notNull(),
views: integer('views').default(0).notNull(),
});
Drizzle infers TypeScript types from those table definitions — typeof users.$inferSelect is the row type, typeof users.$inferInsert is the insert shape. You never run a generate step to keep your client in sync with your schema, because the schema is the source of types.
Because schema is code, you can compose it: a tenantScoped helper that adds tenant_id and a partial index, shared columns via spread, conditional indexes by environment. Things that are awkward in a declarative DSL are normal here.
Queries Look Like SQL Because They Are SQL
The query API is intentionally close to SQL. If you can read a query, you can read Drizzle.
import { eq, and, isNull, desc } from 'drizzle-orm';
import { db } from './db';
import { users, posts } from './schema';
const recent = await db
.select({ id: users.id, email: users.email })
.from(users)
.where(and(eq(users.tenantId, tenantId), isNull(users.deletedAt)))
.orderBy(desc(users.createdAt))
.limit(50);
The shape of the returned object is inferred from the select. If you add name: users.name, the type updates without you doing anything. If you misspell a column or compare a UUID to an integer, it is a compile error.
Joins are explicit, exactly like SQL:
const rows = await db
.select({
postId: posts.id,
title: posts.title,
authorEmail: users.email,
})
.from(posts)
.innerJoin(users, eq(users.id, posts.authorId))
.where(eq(posts.published, true));
This is good and bad. Good: there is no hidden N+1, because there is no relation magic. Bad: you write the joins yourself, every time, until you reach for the relations API.
The Relations API For Nested Reads
When you do want Prisma-style nested reads, Drizzle has the relations() helper and a separate db.query API:
import { relations } from 'drizzle-orm';
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));
const withPosts = await db.query.users.findMany({
with: { posts: { columns: { id: true, title: true } } },
where: eq(users.tenantId, tenantId),
});
Drizzle generates a single SQL query for this — typically using a LEFT JOIN LATERAL with json_agg on Postgres so the nested data comes back already grouped. That is a different shape from what Prisma does (two batched queries), and it shows when you look at your query logs.
Use the relations API for reads that match a nested shape. Use the SQL-shaped builder for everything else.
Migrations With drizzle-kit
drizzle-kit is the CLI that diffs your schema.ts against the database and generates SQL migrations.
# Generate a new migration from schema changes
npx drizzle-kit generate
# Apply pending migrations (you can also run them from code)
npx drizzle-kit migrate
The generated files are plain SQL with an up/down style structure stored as numbered .sql files plus a JSON metadata snapshot. You commit them, you read them, you can edit them. There is no opaque artifact in the loop.
For programmatic migration on app startup, Drizzle exposes a migrate() helper:
import { migrate } from 'drizzle-orm/node-postgres/migrator';
await migrate(db, { migrationsFolder: './drizzle' });
A small but real win: because migrations are plain SQL, they review well in pull requests. Reviewers can spot a DROP COLUMN that nobody intended without running anything.
The Edge Runtime Story
Drizzle was built with multiple runtimes in mind. The same query code runs against:
pg/node-postgresfor classic Node.postgres(thepostgres.jslibrary) for a smaller, faster Node client.@neondatabase/serverlessfor HTTP-over-WebSocket from edge functions.@vercel/postgres, Cloudflare D1, libSQL, PlanetScale's MySQL HTTP API.
You change the driver import. Your queries do not move. That is the practical advantage when you deploy to Vercel Edge or Cloudflare Workers — there is no Rust binary to ship, no engine to start, no cold-start tax on the first query.
// Node
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
export const db = drizzle(new Pool({ connectionString: process.env.DATABASE_URL }));
// Cloudflare Workers / Neon HTTP
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
export const db = drizzle(neon(process.env.DATABASE_URL!));
Where Prisma Is Still The Better Fit
Drizzle is honest about being closer to a query builder than a classic ORM. That means:
- No
prisma studioequivalent that ships in the box. Drizzle Studio exists and works well, but the polish is younger. - Schema migrations from a non-empty database are less guided. Prisma's introspection of an existing database is more turnkey.
- Less hand-holding for newcomers who do not know SQL. That is by design, but if your team is full-stack JavaScript engineers without database depth, Prisma's safety net is real.
If your team writes SQL fluently or you ship to the edge, Drizzle pays. If your team wants to think in models and let the ORM handle the SQL, Prisma is still a great choice.
A One-Sentence Mental Model
Drizzle is a typed SQL builder with a migration tool — you keep the database visible in your code, and the abstraction never gets between you and a query plan.






