Drizzle ORM: SQL in TypeScript Without the Abstraction Tax

I used Prisma for two years. It is a good ORM. The developer experience is polished, the documentation is excellent, and the generated client provides strong type safety. But over time, two things started bothering me: the abstraction layer between me and my SQL, and the generated client that added hundreds of kilobytes to my bundle. Then I found Drizzle, and I have not looked back.
Drizzle's Philosophy
Drizzle takes the position that SQL is not a problem to be solved. SQL is the solution. If you know SQL (and as a backend developer, you should), the ORM should let you write SQL-like syntax in TypeScript with full type safety rather than inventing a completely new query language that you have to mentally translate back to SQL.
Prisma's approach is: "forget SQL, learn our API." Drizzle's approach is: "you already know SQL, here it is in TypeScript." This philosophical difference matters in practice because when you need to optimize a query, debug slow performance, or write something complex, Drizzle's output maps directly to the SQL you would write by hand. With Prisma, you are guessing at what SQL the generated client produces.
Defining Schemas in TypeScript
In Drizzle, your database schema is defined in TypeScript files. No special schema files, no code generation step, no generated client. Here is a practical schema:
import { pgTable, serial, text, timestamp, integer } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
export const posts = pgTable("posts", {
id: serial("id").primaryKey(),
title: text("title").notNull(),
content: text("content").notNull(),
authorId: integer("author_id").references(() => users.id).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
export const comments = pgTable("comments", {
id: serial("id").primaryKey(),
body: text("body").notNull(),
postId: integer("post_id").references(() => posts.id).notNull(),
authorId: integer("author_id").references(() => users.id).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
comments: many(comments),
}));
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
comments: many(comments),
}));
export const commentsRelations = relations(comments, ({ one }) => ({
post: one(posts, {
fields: [comments.postId],
references: [posts.id],
}),
author: one(users, {
fields: [comments.authorId],
references: [users.id],
}),
}));
This is just TypeScript. Your editor gives you autocomplete, go-to-definition, and refactoring tools that work out of the box. No language server plugin for a custom schema language needed.
Queries That Look Like SQL
Here is where the philosophy pays off. Drizzle queries map directly to SQL concepts:
import { db } from "./db";
import { users, posts, comments } from "./schema";
import { eq, desc, count, sql } from "drizzle-orm";
// Simple select
const allUsers = await db.select().from(users);
// Filtered query with ordering
const recentPosts = await db
.select()
.from(posts)
.where(eq(posts.authorId, 1))
.orderBy(desc(posts.createdAt))
.limit(10);
// Join
const postsWithAuthors = await db
.select({
postTitle: posts.title,
authorName: users.name,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id));
// Aggregation
const commentCounts = await db
.select({
postId: comments.postId,
commentCount: count(comments.id),
})
.from(comments)
.groupBy(comments.postId);
// Insert
const newUser = await db
.insert(users)
.values({ name: "Bryan", email: "[email protected]" })
.returning();
If you know SQL, you can read every one of these queries without any documentation. `select().from().where().orderBy()` is just SQL in method chain form. Every query returns fully typed results. The `postsWithAuthors` variable is typed as `{ postTitle: string; authorName: string }[]` automatically.
Relational Queries
For cases where you want Prisma-style nested includes, Drizzle has a relational query API:
const usersWithPosts = await db.query.users.findMany({
with: {
posts: {
with: {
comments: true,
},
},
},
});
This gives you the convenience of nested data loading while still generating efficient SQL under the hood. The result type is fully inferred, including all nested relations.
Drizzle Kit for Migrations
Drizzle Kit handles schema migrations. You change your TypeScript schema, then run the generate command followed by the migrate command. The generate step diffs your TypeScript schema against the current database state and produces a SQL migration file. The migrate step applies it. You can also use push for rapid prototyping, which applies schema changes directly without generating migration files.
The migration files are plain SQL. You can read them, edit them, and commit them to version control. There is no binary migration format or opaque migration engine.
Drizzle-Zod for Validation
The drizzle-zod package generates Zod schemas from your Drizzle table definitions:
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
const insertUserSchema = createInsertSchema(users);
const selectUserSchema = createSelectSchema(users);
// Use in your API routes
app.post("/users", async (req, res) => {
const parsed = insertUserSchema.parse(req.body);
const user = await db.insert(users).values(parsed).returning();
res.json(user[0]);
});
One schema definition powers your database, your types, and your validation. Change the schema in one place and everything updates. No syncing between separate schema files, Zod schemas, and TypeScript types.
Drizzle vs Prisma: The Real Comparison
Query builder vs client generation. Drizzle is a query builder. You write queries using its TypeScript API, and it generates SQL. No code generation step. Prisma generates a client from your schema file. Every time you change the schema, you run a generate command. This means Prisma has a build step that Drizzle does not.
Bundle size. Drizzle's core is about 30KB gzipped. Prisma's generated client varies but typically ranges from 300KB to over 1MB depending on your schema complexity. For serverless deployments where cold start time correlates with bundle size, this difference matters.
Performance. Drizzle generates straightforward SQL. Prisma's client adds a query engine layer (written in Rust) between your code and the database. In benchmarks, Drizzle consistently shows lower query latency, especially for simple CRUD operations. The difference is usually 1ms to 5ms per query, which adds up in high-throughput applications.
Migration story. Both handle migrations well. Prisma's migration system is more polished with a GUI in Prisma Studio. Drizzle Kit is simpler and produces plain SQL files. I prefer Drizzle's approach because I can read and edit the migration SQL directly.
Learning curve. If you know SQL, Drizzle is easier to learn. If you do not know SQL and want an ORM that abstracts it away, Prisma is more approachable. But I would argue that learning SQL is worth the investment regardless of which ORM you use.
When to Pick Drizzle vs Prisma
Pick Drizzle if you are comfortable with SQL and want your ORM to feel like SQL. Pick it if bundle size matters (serverless, edge deployments). Pick it if you want zero code generation in your build pipeline. Pick it if you prefer defining schemas in TypeScript rather than a custom schema language.
Pick Prisma if you want a more polished overall developer experience with Prisma Studio. Pick it if your team is less comfortable with raw SQL concepts. Pick it if you value the larger ecosystem of Prisma-specific tools and integrations.
For my projects, the answer has been Drizzle across the board. The SQL-like syntax feels natural, the bundle is tiny, the types are excellent, and I never have to think about code generation. It respects the fact that SQL is a good language and just gives me a type-safe way to write it in TypeScript. That is exactly what I want from an ORM.