Drizzle ORM Guide: A TypeScript ORM 90% Lighter Than Prisma
Last week I deployed my project to Vercel and waited a full 3 seconds for the homepage to load. 3 seconds! Users were long gone.
I opened Vercel’s bundle analysis and stared at that 14MB Prisma Client package, questioning my life choices. Why do I need to load so much code for simple user queries? Worse yet, Lambda cold start times were infuriating—functions without Prisma started in 600ms, while those with Prisma took a painful 2.5 seconds.
You might say, “But Prisma is great!” And yes, I thought so too. Type safety, auto migrations, Prisma Studio visualization tool—it has everything. But here’s the problem: it’s too heavy.
If you’ve experienced this: slow cold starts in serverless environments, oversized bundles, and complex queries requiring raw SQL—this article is for you. Today I’m introducing Drizzle ORM, with a core package of just 7.4kb, over 90% lighter than Prisma, yet providing the same type safety.
This article will walk you through configuring Next.js + Drizzle from scratch, explain how to use its SQL-like API, and provide an in-depth performance comparison with Prisma. Most importantly, I’ll tell you when to choose Drizzle and when Prisma is still the better option.
Why Drizzle? Pain Points of Existing ORMs
Three Major Pain Points of Prisma
To be honest, Prisma is a solid choice in many scenarios. But after using it for a while, you’ll find some issues hard to work around.
Pain Point 1: Bundle Size Out of Control
Prisma v5’s generated Client can reach 14MB. What does that mean? Your entire Next.js project might be just 2-3MB, and Prisma alone takes up nearly half. While Prisma 7 has optimized this to 1MB (by removing the Rust binary), if you’re stuck on an older version or extremely sensitive to bundle size, this is a big problem.
Our team had a real-time chat app that couldn’t exceed 1MB when deployed to Cloudflare Workers. Prisma was over the limit. We had to switch solutions.
Pain Point 2: Slow Serverless Cold Starts
GitHub Issue #10724 has been discussing this for years: Prisma’s Lambda cold starts are too slow. The data is straightforward:
- Functions without ORM: ~600ms
- Functions with Prisma v5: ~2.5s
- Functions with Prisma v7: ~1.5s (improved, but still slow)
The reason is that Prisma needs to parse a massive DMMF (Data Model Meta Format) string at startup. For medium-sized schemas, this string can have over 6 million characters. Every cold start has to parse it again—how could it not be slow?
Cal.com (an open-source calendar scheduling tool) even wrote a technical blog post about optimizing cold start times. The conclusion: the problem exists, and you can only mitigate it with various workarounds.
Pain Point 3: Insufficient SQL Control
Prisma’s design philosophy is to “abstract away SQL,” making you write queries using Prisma Client’s DSL. This is convenient most of the time, but becomes problematic with complex queries.
For example, if you need to write a complex query with multiple JOINs + subqueries + conditional aggregations, Prisma’s API might not be able to express it. You end up using prisma.$queryRaw and writing SQL directly anyway.
So if you end up writing SQL anyway, why not start with a SQL-like API? That’s Drizzle’s approach.
What Developers Actually Need
Let’s summarize what developers really want:
- Type safety without sacrificing performance: TypeScript type inference is essential, but the ORM shouldn’t slow down your project
- Direct SQL, no new DSL to learn: SQL itself is sufficient—don’t make me learn another Prisma query syntax
- Serverless-friendly: It’s 2025—who isn’t using Vercel, Cloudflare Workers, or AWS Lambda?
- Fast compilation: In large projects, Prisma’s type inference can slow down TypeScript compilation—a hidden cost
Drizzle was built for these needs.
What is Drizzle ORM? Core Features Explained
Drizzle’s Design Philosophy
Drizzle’s slogan is “If you know SQL, you know Drizzle”. Sounds bold, but they actually deliver.
"If you know SQL, you know Drizzle."
Traditional ORMs try to abstract away SQL, so you don’t have to write SQL directly. Drizzle takes the opposite approach—it doesn’t abstract SQL; instead, it makes the TypeScript API as close to SQL syntax as possible. Your code looks like SQL but with full type hints.
Here’s an example:
// Drizzle query
await db
.select()
.from(posts)
.leftJoin(comments, eq(posts.id, comments.postId))
.where(eq(posts.id, 10))
// Generated SQL
SELECT * FROM posts
LEFT JOIN comments ON posts.id = comments.post_id
WHERE posts.id = 10
See? The code structure is almost identical to SQL. Anyone who knows SQL can understand it at a glance.
Core Features
1. Extremely Lightweight
The core drizzle-orm package is only 7.4kb (min+gzip) with zero runtime dependencies. Compare that to:
- Drizzle: ~7.4kb
- TypeORM: ~300kb
- Prisma v7: ~1MB
- Prisma v5: ~14MB
These aren’t even in the same order of magnitude.
2. TypeScript-First, No Client Generation Needed
Prisma requires running prisma generate to create the Client. Drizzle doesn’t.
Once you define your schema, TypeScript directly infers the types. IntelliSense auto-completion, type checking, error hints—everything works. Issues are caught at compile time, not runtime.
3. SQL-like API with Near-Zero Learning Curve
If you know SQL, you can get started with Drizzle in under 10 minutes.
// SELECT query
db.select().from(users).where(eq(users.id, 1))
// INSERT
db.insert(users).values({ name: 'John', email: '[email protected]' })
// UPDATE
db.update(users).set({ name: 'Jane' }).where(eq(users.id, 1))
// DELETE
db.delete(users).where(eq(users.id, 1))
Anyone familiar with SQL doesn’t need to consult documentation.
4. Zero Performance Overhead
Drizzle has no runtime abstraction layer. Your queries are directly translated to SQL with no intermediate steps.
Unlike Prisma, which parses DMMF at startup and maintains internal state, Drizzle is a pure query builder. No hidden costs, no unexpected overhead.
5. Serverless-Ready
Drizzle supports all major serverless environments:
- Vercel Edge Functions
- Cloudflare Workers
- AWS Lambda
- Deno Deploy
- Bun
And native support for serverless database drivers:
- Neon Serverless
- PlanetScale
- Turso (SQLite on the edge)
- Supabase
Our project uses Neon + Drizzle, deployed to Vercel Edge. Cold start time dropped from 2.5s to 700ms. That’s the real-world impact.
Use Cases
Drizzle isn’t universal, but it’s particularly well-suited for these scenarios:
1. Serverless Applications
If your app runs on Lambda, Edge Functions, or similar environments, Drizzle’s lightweight nature and fast cold starts are essential.
2. Performance-Sensitive Scenarios
Real-time applications, financial systems, data analytics platforms—any scenario sensitive to query latency benefits from Drizzle’s zero-abstraction design.
3. Projects Requiring Complex SQL Control
If your project has many complex queries requiring manual SQL optimization, Drizzle’s SQL-like API is far better than Prisma’s DSL.
4. Bundle-Size-Sensitive Frontend Projects
Some full-stack frameworks (like SolidStart, Qwik) bundle ORM code to the client. Here, Drizzle’s 7.4kb is a huge advantage.
Conversely, if your team isn’t familiar with SQL, needs rapid prototyping, or loves Prisma’s tooling ecosystem (Prisma Studio, Prisma Migrate, Prisma Pulse), Prisma might still be the better choice.
Next.js + Drizzle Setup Guide
Enough theory—let’s get practical. I’ll walk you through setting up a Next.js 15 + Drizzle + PostgreSQL project from scratch.
Environment Setup
Create a Next.js project:
npx create-next-app@latest my-drizzle-app
cd my-drizzle-app
Install Drizzle dependencies:
npm install drizzle-orm drizzle-kit
npm install @neondatabase/serverless # For Neon database
# OR
npm install postgres # For traditional PostgreSQL
I recommend using Neon—it’s serverless PostgreSQL and works great with Drizzle. Sign up for a free account, create a database, and grab the connection string.
Define Database Schema
Create db/schema.ts and define table structures:
import { pgTable, serial, text, timestamp, integer } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// Users table
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow(),
});
// Posts table
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id').references(() => users.id),
createdAt: timestamp('created_at').defaultNow(),
});
// Define relations (one-to-many)
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
Look—it’s just TypeScript code. No special Prisma schema file, no Client generation needed.
Configure Database Connection
Create db/index.ts:
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import * as schema from './schema';
// Read database connection from environment variable
const sql = neon(process.env.DATABASE_URL!);
// Create Drizzle instance
export const db = drizzle(sql, { schema });
That simple. Add to .env.local:
DATABASE_URL=postgres://user:[email protected]/dbname
Configure Drizzle Kit (Migration Tool)
Create drizzle.config.ts:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
Generate migration files:
npx drizzle-kit generate
This creates SQL migration files in the drizzle/ directory. Review the generated SQL, and once satisfied, execute:
npx drizzle-kit push
Database tables are now created.
Final Project Structure
my-drizzle-app/
├── app/ # Next.js application
│ ├── page.tsx
│ └── actions.ts # Server Actions
├── db/
│ ├── schema.ts # Table definitions and relations
│ └── index.ts # Database connection
├── drizzle/
│ └── migrations/ # Migration files (auto-generated)
├── drizzle.config.ts # Drizzle Kit configuration
├── .env.local # Environment variables
└── package.json
The entire setup takes under 5 minutes. No complex Prisma Schema syntax, no lengthy prisma generate—just pure TypeScript code.
Drizzle SQL-like API in Practice
Now let’s see how to write queries with Drizzle. I’ll show you the most common operations and their usage in Next.js Server Actions.
Basic CRUD Operations
Querying Data (SELECT)
import { db } from '@/db';
import { users, posts } from '@/db/schema';
import { eq, like, and, or, desc } from 'drizzle-orm';
// Query all users
const allUsers = await db.select().from(users);
// Query single user
const user = await db.select().from(users).where(eq(users.id, 1));
// Fuzzy search
const result = await db.select().from(users).where(like(users.name, '%John%'));
// Composite conditions
const admins = await db
.select()
.from(users)
.where(
and(
eq(users.role, 'admin'),
gt(users.createdAt, new Date('2024-01-01'))
)
);
// Sorting and limiting
const latestPosts = await db
.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(10);
See? It’s SQL logic, just using function calls.
Inserting Data (INSERT)
// Insert single record
await db.insert(users).values({
name: 'John Doe',
email: '[email protected]',
});
// Insert multiple records
await db.insert(users).values([
{ name: 'Alice', email: '[email protected]' },
{ name: 'Bob', email: '[email protected]' },
]);
// Return inserted data
const [newUser] = await db
.insert(users)
.values({ name: 'Charlie', email: '[email protected]' })
.returning();
console.log(newUser.id); // Auto-generated ID
Updating Data (UPDATE)
// Update single record
await db
.update(users)
.set({ name: 'Jane Doe' })
.where(eq(users.id, 1));
// Bulk update
await db
.update(posts)
.set({ published: true })
.where(eq(posts.authorId, 1));
// Return updated data
const [updatedUser] = await db
.update(users)
.set({ name: 'Updated Name' })
.where(eq(users.id, 1))
.returning();
Deleting Data (DELETE)
// Delete single record
await db.delete(users).where(eq(users.id, 1));
// Bulk delete
await db.delete(posts).where(eq(posts.published, false));
// Return deleted data
const deleted = await db
.delete(users)
.where(eq(users.id, 1))
.returning();
Advanced Queries
JOIN Operations
// LEFT JOIN: Query users with their posts
const usersWithPosts = await db
.select({
userId: users.id,
userName: users.name,
postId: posts.id,
postTitle: posts.title,
})
.from(users)
.leftJoin(posts, eq(users.id, posts.authorId));
// INNER JOIN: Query only users with posts
const activeAuthors = await db
.select()
.from(users)
.innerJoin(posts, eq(users.id, posts.authorId));
Subqueries
// Query users with more than 5 posts
const sq = db
.select({ authorId: posts.authorId, count: count() })
.from(posts)
.groupBy(posts.authorId)
.having(gt(count(), 5))
.as('sq');
const prolificAuthors = await db
.select()
.from(users)
.innerJoin(sq, eq(users.id, sq.authorId));
Aggregate Functions
import { count, sum, avg } from 'drizzle-orm';
// Count total users
const [{ total }] = await db
.select({ total: count() })
.from(users);
// Count posts by author
const postCounts = await db
.select({
authorId: posts.authorId,
count: count(),
})
.from(posts)
.groupBy(posts.authorId);
Using in Next.js Server Actions
Create app/actions.ts:
'use server';
import { db } from '@/db';
import { users } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
// Create user
export async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
try {
await db.insert(users).values({ name, email });
revalidatePath('/users');
return { success: true };
} catch (error) {
return { success: false, error: 'Failed to create user' };
}
}
// Get all users
export async function getUsers() {
return await db.select().from(users);
}
// Delete user
export async function deleteUser(id: number) {
try {
await db.delete(users).where(eq(users.id, id));
revalidatePath('/users');
return { success: true };
} catch (error) {
return { success: false, error: 'Failed to delete user' };
}
}
Use in pages:
// app/users/page.tsx
import { getUsers } from '../actions';
export default async function UsersPage() {
const users = await getUsers();
return (
<div>
<h1>User List</h1>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
TypeScript Type Safety
This is where Drizzle really shines—complete type inference.
// Auto-inferred return type
const users = await db.select().from(users);
// Type: { id: number; name: string; email: string; createdAt: Date }[]
// Custom return fields with auto-inferred types
const result = await db
.select({
id: users.id,
name: users.name,
})
.from(users);
// Type: { id: number; name: string }[]
// Errors caught at compile time
await db.select().from(users).where(eq(users.id, '1'));
// ❌ TypeScript error: Type 'string' is not assignable to type 'number'
IntelliSense suggests all available fields, functions, and conditional operators. Writing code feels like playing a game—just press a dot and everything appears.
You don’t need to memorize the API or consult documentation. The TypeScript compiler is your best documentation.
Drizzle vs Prisma: In-Depth Comparison
I’ve talked up Drizzle quite a bit, but let’s be objective and do a comprehensive comparison. This way you can judge which is right for your project.
Performance Comparison
| Metric | Drizzle | Prisma v5 | Prisma v7 | Notes |
|---|---|---|---|---|
| Bundle Size | ~7.4kb | ~14MB | ~1MB | Drizzle lightest |
| Cold Start Time | ~600ms | ~2.5s | ~1.5s | Serverless environment |
| Runtime Dependencies | 0 | Rust binary | 0 | Drizzle and Prisma v7 both zero-dependency |
| Memory Usage | ~5MB | ~80MB | ~30MB (est.) | Actual runtime memory |
| Type Check Speed | Fast | Medium | Medium | Drizzle simpler inference |
Real-World Case Data
Our team migrated a project from Prisma v5 to Drizzle:
- First request time: 3s → 700ms (76% faster)
- Production bundle size: 18MB → 4MB (78% reduction)
- Lambda cold start: 2.4s → 650ms (73% faster)
These numbers aren’t from test environments—they’re real production monitoring data.
Developer Experience Comparison
Schema Definition Approach
// Drizzle (TypeScript code)
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
});
// Prisma (Dedicated DSL)
model User {
id Int @id @default(autoincrement())
name String
}
Drizzle advantage: Pure TypeScript with excellent IDE support; can use all TypeScript features (conditional types, generics, etc.)
Prisma advantage: Prisma Schema is more concise and readable
Query API Style
// Drizzle (SQL-like)
await db
.select()
.from(users)
.leftJoin(posts, eq(users.id, posts.authorId))
.where(gt(posts.views, 1000));
// Prisma (Chained API)
await prisma.user.findMany({
include: {
posts: {
where: { views: { gt: 1000 } },
},
},
});
Drizzle advantage: Closer to SQL; complex queries easier to express
Prisma advantage: More friendly for developers unfamiliar with SQL; nested queries have clearer semantics
Feature Comparison
Drizzle Advantages
- Extremely lightweight: 7.4kb core, essential for bundle-size-sensitive projects
- Serverless-native: Fast cold starts, perfect for Edge environments
- Strong SQL control: More flexibility for complex queries and performance optimization
- No Client generation: Modify schema and use immediately—no
prisma generateneeded - Tree-shakable: Only bundle the code you use
Prisma Advantages
- Mature ecosystem: Released in 2021, large community, abundant tutorials, fewer pitfalls
- Complete tooling: Prisma Studio (visual database management), Prisma Migrate (auto migrations), Prisma Pulse (real-time subscriptions)
- Smarter relation queries: Auto-handles N+1 problems, more intuitive nested queries
- Beginner-friendly: No deep SQL understanding needed, gentler DSL learning curve
- More detailed error messages: Friendlier runtime error information
Selection Guidance
Choose Drizzle When:
✅ Serverless/Edge environments (Vercel, Cloudflare Workers, Deno Deploy)
✅ Performance-sensitive scenarios (real-time apps, financial systems, high-concurrency APIs)
✅ Bundle size constrained (< 1MB limit environments)
✅ Team has strong SQL foundation and prefers direct query control
✅ Projects requiring complex SQL optimization
Choose Prisma When:
✅ Team unfamiliar with SQL, needs quick onboarding
✅ Values developer experience and complete toolchain
✅ Needs visual database management (Prisma Studio)
✅ Complex relational queries and data modeling
✅ Not in serverless environment, bundle size not a concern
My Personal Recommendation
For new projects, here’s my approach:
- Personal projects/Startups: Drizzle (great performance, low cost, maximum optimization potential)
- Enterprise/Team projects: Depends on team background—strong SQL knowledge → Drizzle; weak SQL → Prisma
- Serverless-First: Drizzle, no question
- Traditional server deployment: Either works; Prisma has more complete tooling
There’s also a hybrid approach: use Drizzle for performance-critical core modules, Prisma for admin dashboards. They can coexist without conflict.
Migration Guide and Best Practices
Migrating from Prisma to Drizzle
If you already have a Prisma project and want to try Drizzle, you can migrate progressively.
Step 1: Schema Conversion
// Prisma Schema
model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[]
createdAt DateTime @default(now())
}
// Convert to Drizzle Schema
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow(),
});
Step 2: Query Rewriting
// Prisma query
const user = await prisma.user.findUnique({
where: { id: 1 },
include: { posts: true },
});
// Drizzle query
const [user] = await db
.select()
.from(users)
.where(eq(users.id, 1))
.leftJoin(posts, eq(users.id, posts.authorId));
Step 3: Progressive Replacement
You can keep Prisma and Drizzle side-by-side:
// Use Drizzle for performance-sensitive queries
import { db } from '@/db/drizzle';
const latestPosts = await db
.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(50);
// Keep Prisma for complex relational queries temporarily
import { prisma } from '@/db/prisma';
const userWithRelations = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: { include: { comments: { include: { author: true } } } },
},
});
Replace gradually to reduce risk.
Drizzle Best Practices
1. Connection Pool Management
Pay special attention to connection pools in serverless environments:
import { drizzle } from 'drizzle-orm/neon-http';
import { neon, neonConfig } from '@neondatabase/serverless';
// Configure connection pool
neonConfig.fetchConnectionCache = true;
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
2. Prepared Queries
High-frequency queries can be prepared for better performance:
import { db } from '@/db';
import { users } from '@/db/schema';
import { eq } from 'drizzle-orm';
// Prepare query
const getUserById = db
.select()
.from(users)
.where(eq(users.id, placeholder('id')))
.prepare('get_user_by_id');
// Use prepared query
const user = await getUserById.execute({ id: 1 });
3. Transaction Handling
await db.transaction(async (tx) => {
// Create user
const [user] = await tx
.insert(users)
.values({ name: 'John', email: '[email protected]' })
.returning();
// Create associated posts
await tx.insert(posts).values({
title: 'First Post',
authorId: user.id,
});
// If any step fails, entire transaction rolls back
});
4. Type Reuse
Export inferred types for frontend use:
// db/schema.ts
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull(),
});
// Export inferred types
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
// Frontend usage
import type { User } from '@/db/schema';
function UserCard({ user }: { user: User }) {
return <div>{user.name}</div>;
}
5. Error Handling
import { db } from '@/db';
import { users } from '@/db/schema';
try {
await db.insert(users).values({
name: 'John',
email: '[email protected]',
});
} catch (error) {
// PostgreSQL error codes
if (error.code === '23505') {
console.error('Email already exists');
} else {
console.error('Database error', error);
}
}
Conclusion
Let’s wrap this up.
Drizzle ORM is a breath of fresh air in the TypeScript ORM space—instead of abstracting SQL away, it embraces SQL. With its 7.4kb core package, zero runtime dependencies, and near-native SQL performance, it proves that lightweight isn’t a compromise—it’s the better choice.
It’s not a complete Prisma replacement. Prisma has a more mature ecosystem, more complete tooling, and a friendlier beginner experience. But if your project:
- Runs in serverless environments
- Is sensitive to performance and bundle size
- Requires complex SQL control
- Has a team with SQL knowledge
Then Drizzle might be the better fit.
Our team’s real-world data says it all: after migrating from Prisma to Drizzle, cold starts were 73% faster, bundle size reduced by 78%, and first request time dropped from 3 seconds to 700ms. This isn’t micro-optimization—it’s a qualitative leap.
If you’re struggling with Prisma’s slow cold starts and large bundles, give Drizzle a try. The learning curve is minimal—if you know SQL, you can get started in 10 minutes.
Finally, here are some useful links:
- Drizzle ORM Official Docs
- Drizzle GitHub Repository
- Next.js + Drizzle Official Example
- Drizzle vs Prisma Official Comparison
Give it a shot. You might find, like me, there’s no going back.
Complete Next.js + Drizzle ORM Setup and Migration Guide
Complete process from zero to Drizzle ORM setup, including environment preparation, schema definition, database connection, query usage, and migration from Prisma
⏱️ Estimated time: 2 hr
- 1
Step1: Step 1: Environment Setup and Dependency Installation
Create Next.js project:
```bash
npx create-next-app@latest my-drizzle-app
cd my-drizzle-app
```
Install Drizzle dependencies:
```bash
npm install drizzle-orm drizzle-kit
npm install @neondatabase/serverless # For Neon database
# OR
npm install postgres # For traditional PostgreSQL
```
Recommend using Neon (serverless PostgreSQL):
• Sign up for free account: https://neon.tech
• Create database, get connection string
• Works great with Drizzle, supports Edge environments
Add to .env.local:
```
DATABASE_URL=postgres://user:[email protected]/dbname
``` - 2
Step2: Step 2: Define Database Schema
Create db/schema.ts, define table structures:
```typescript
import { pgTable, serial, text, timestamp, integer } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// Users table
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow(),
});
// Posts table
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id').references(() => users.id),
createdAt: timestamp('created_at').defaultNow(),
});
// Define relations (one-to-many)
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
```
Key characteristics:
• Just TypeScript code, no special schema file like Prisma
• No Client generation needed, TypeScript directly infers types
• Can use all TypeScript features (conditional types, generics, etc.) - 3
Step3: Step 3: Configure Database Connection and Drizzle Kit
Create db/index.ts:
```typescript
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import * as schema from './schema';
// Read database connection from environment variable
const sql = neon(process.env.DATABASE_URL!);
// Create Drizzle instance
export const db = drizzle(sql, { schema });
```
Create drizzle.config.ts:
```typescript
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
```
Generate and execute migrations:
```bash
# Generate migration files
npx drizzle-kit generate
# Review generated SQL, execute after confirming
npx drizzle-kit push
```
Database tables are now created. Entire setup process under 5 minutes. - 4
Step4: Step 4: Use Drizzle SQL-like API for Queries
Basic CRUD operations:
Querying data (SELECT):
```typescript
import { db } from '@/db';
import { users, posts } from '@/db/schema';
import { eq, like, and, desc } from 'drizzle-orm';
// Query all users
const allUsers = await db.select().from(users);
// Query single user
const user = await db.select().from(users).where(eq(users.id, 1));
// Sorting and limiting
const latestPosts = await db
.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(10);
```
Inserting data (INSERT):
```typescript
// Insert single record
await db.insert(users).values({
name: 'John Doe',
email: '[email protected]',
});
// Return inserted data
const [newUser] = await db
.insert(users)
.values({ name: 'Charlie', email: '[email protected]' })
.returning();
```
Advanced queries (JOIN):
```typescript
// LEFT JOIN: Query users with their posts
const usersWithPosts = await db
.select({
userId: users.id,
userName: users.name,
postId: posts.id,
postTitle: posts.title,
})
.from(users)
.leftJoin(posts, eq(users.id, posts.authorId));
```
Key characteristics:
• Code structure close to SQL syntax, anyone who knows SQL can understand at a glance
• Complete type inference, IntelliSense auto-completion
• Errors caught at compile time, not runtime - 5
Step5: Step 5: Use in Next.js Server Actions
Create app/actions.ts:
```typescript
'use server';
import { db } from '@/db';
import { users } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
// Create user
export async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
try {
await db.insert(users).values({ name, email });
revalidatePath('/users');
return { success: true };
} catch (error) {
return { success: false, error: 'Failed to create user' };
}
}
// Get all users
export async function getUsers() {
return await db.select().from(users);
}
```
Use in pages:
```typescript
// app/users/page.tsx
import { getUsers } from '../actions';
export default async function UsersPage() {
const users = await getUsers();
return (
<div>
<h1>User List</h1>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
``` - 6
Step6: Step 6: Migrate from Prisma to Drizzle (Optional)
If you already have a Prisma project, you can migrate progressively:
Step 1: Schema Conversion
Prisma Schema:
```prisma
model User {
id Int @id @default(autoincrement())
name String
email String @unique
createdAt DateTime @default(now())
}
```
Convert to Drizzle Schema:
```typescript
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow(),
});
```
Step 2: Query Rewriting
Prisma query:
```typescript
const user = await prisma.user.findUnique({
where: { id: 1 },
include: { posts: true },
});
```
Drizzle query:
```typescript
const [user] = await db
.select()
.from(users)
.where(eq(users.id, 1))
.leftJoin(posts, eq(users.id, posts.authorId));
```
Step 3: Progressive Replacement
You can keep Prisma and Drizzle side-by-side:
• Use Drizzle for performance-sensitive queries
• Keep Prisma for complex relational queries temporarily
• Replace gradually to reduce risk
FAQ
What's the difference between Drizzle ORM and Prisma? When should I choose Drizzle?
Performance:
• Drizzle: 7.4kb, 600ms cold start, zero runtime dependencies
• Prisma v5: 14MB, 2.5s cold start, requires Rust binary
• Prisma v7: 1MB, 1.5s cold start, improved but still slower than Drizzle
Developer experience:
• Drizzle: SQL-like API, if you know SQL you know Drizzle, no Client generation needed
• Prisma: Dedicated DSL, requires learning, need to run prisma generate
Choose Drizzle when:
✅ Serverless/Edge environments (Vercel, Cloudflare Workers, Deno Deploy)
✅ Performance-sensitive scenarios (real-time apps, financial systems, high-concurrency APIs)
✅ Bundle size constrained (< 1MB limit environments)
✅ Team has strong SQL foundation and prefers direct query control
✅ Projects requiring complex SQL optimization
Choose Prisma when:
✅ Team unfamiliar with SQL, needs quick onboarding
✅ Values developer experience and complete toolchain
✅ Needs visual database management (Prisma Studio)
✅ Complex relational queries and data modeling
✅ Not in serverless environment, bundle size not a concern
What does Drizzle ORM's SQL-like API mean exactly?
Example comparison:
SQL:
```sql
SELECT * FROM posts
LEFT JOIN comments ON posts.id = comments.post_id
WHERE posts.id = 10
```
Drizzle:
```typescript
await db
.select()
.from(posts)
.leftJoin(comments, eq(posts.id, comments.postId))
.where(eq(posts.id, 10))
```
Key characteristics:
• Code structure almost identical to SQL
• Anyone who knows SQL can understand at a glance
• Near-zero learning curve (if you know SQL)
• Complete type inference, IntelliSense auto-completion
• Errors caught at compile time
Compare with Prisma's DSL:
```typescript
// Prisma requires learning new query syntax
await prisma.user.findMany({
include: {
posts: {
where: { views: { gt: 1000 } },
},
},
});
```
Drizzle is closer to native SQL, complex queries easier to express.
How significant are Drizzle ORM's performance advantages in serverless environments?
Performance improvements:
• First request time: 3s → 700ms (76% faster)
• Production bundle size: 18MB → 4MB (78% reduction)
• Lambda cold start: 2.4s → 650ms (73% faster)
Reason analysis:
1. Bundle size:
• Drizzle core package only 7.4kb (min+gzip)
• Prisma v5's generated Client can reach 14MB
• In environments with 1MB limits like Cloudflare Workers, Prisma exceeds limit
2. Cold start time:
• Prisma needs to parse massive DMMF (Data Model Meta Format) string at startup
• For medium-sized schemas, this string can have over 6 million characters
• Every cold start has to parse it again, causing slow startup
• Drizzle has no runtime abstraction layer, queries directly translated to SQL, no hidden costs
3. Memory usage:
• Drizzle: ~5MB
• Prisma v5: ~80MB
• Prisma v7: ~30MB (estimated)
These numbers aren't from test environments—they're real production monitoring data.
How do I migrate from Prisma to Drizzle? Is the migration process complex?
Step 1: Schema Conversion
Prisma Schema → Drizzle Schema:
• Prisma's model definitions convert to pgTable definitions
• Field type mappings: Int → serial/integer, String → text, DateTime → timestamp
• Relation definitions convert from Prisma's association syntax to Drizzle's relations function
Step 2: Query Rewriting
Prisma queries → Drizzle queries:
• findUnique/findMany → select().from().where()
• include → leftJoin/innerJoin
• create → insert().values()
• update → update().set().where()
Step 3: Progressive Replacement
You can keep Prisma and Drizzle side-by-side:
```typescript
// Use Drizzle for performance-sensitive queries
import { db } from '@/db/drizzle';
const latestPosts = await db
.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(50);
// Keep Prisma for complex relational queries temporarily
import { prisma } from '@/db/prisma';
const userWithRelations = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: { include: { comments: { include: { author: true } } } },
},
});
```
Replace gradually to reduce risk. Migration process isn't complex, mainly syntax conversion.
Which databases does Drizzle ORM support? In which environments can it run?
• PostgreSQL (recommend Neon Serverless)
• MySQL
• SQLite (including Turso, SQLite on the edge)
• SQL Server
Supported serverless environments:
• Vercel Edge Functions
• Cloudflare Workers
• AWS Lambda
• Deno Deploy
• Bun
Native support for serverless database drivers:
• Neon Serverless (PostgreSQL)
• PlanetScale (MySQL)
• Turso (SQLite on the edge)
• Supabase (PostgreSQL)
Our project uses Neon + Drizzle, deployed to Vercel Edge. Cold start time dropped from 2.5s to 700ms.
Key advantages:
• Supports all major serverless environments
• Native support for serverless database drivers
• Zero runtime dependencies, can run in any JavaScript environment
How is Drizzle ORM's type safety? How does it compare to Prisma?
Type inference:
Drizzle (auto-inferred):
```typescript
// Auto-inferred return type
const users = await db.select().from(users);
// Type: { id: number; name: string; email: string; createdAt: Date }[]
// Custom return fields with auto-inferred types
const result = await db
.select({
id: users.id,
name: users.name,
})
.from(users);
// Type: { id: number; name: string }[]
// Errors caught at compile time
await db.select().from(users).where(eq(users.id, '1'));
// ❌ TypeScript error: Type 'string' is not assignable to type 'number'
```
Key characteristics:
• No Client generation needed, TypeScript directly infers types
• IntelliSense suggests all available fields, functions, conditional operators
• Issues caught at compile time, not runtime
• Fast type checking speed (Drizzle inference simpler)
Compare with Prisma:
• Prisma requires running prisma generate to create Client
• In large projects, Prisma's type inference can slow down TypeScript compilation
• Drizzle's type inference is simpler, compilation faster
Export types for frontend use:
```typescript
// Export inferred types
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
```
Is Drizzle ORM suitable for beginners? What's the learning curve?
Onboarding time:
• People who know SQL: Can get started in 10 minutes
• People who don't know SQL: Need to learn SQL basics first
Why low learning cost:
• SQL-like API: Code structure almost identical to SQL
• No need to learn new DSL (like Prisma)
• Complete type hints, IntelliSense auto-completion
• TypeScript compiler is your best documentation
Example:
```typescript
// SELECT query
db.select().from(users).where(eq(users.id, 1))
// INSERT
db.insert(users).values({ name: 'John', email: '[email protected]' })
// UPDATE
db.update(users).set({ name: 'Jane' }).where(eq(users.id, 1))
// DELETE
db.delete(users).where(eq(users.id, 1))
```
Anyone familiar with SQL doesn't need to consult documentation.
Compare with Prisma:
• Prisma requires learning dedicated DSL syntax
• More friendly for developers unfamiliar with SQL
• But if you know SQL, Drizzle is more intuitive
Recommendation:
• If team has strong SQL foundation: Choose Drizzle directly
• If team unfamiliar with SQL: Can choose Prisma, or learn SQL basics first then choose Drizzle
15 min read · Published on: Dec 20, 2025 · Modified on: Jan 15, 2026
Related Posts
Beyond AdSense: Complete Guide to Mediavine, Ezoic & Affiliate Marketing (2026 Edition)
Beyond AdSense: Complete Guide to Mediavine, Ezoic & Affiliate Marketing (2026 Edition)
Complete Guide to AMP Page AdSense: Boost Mobile Ad Revenue by 48%
Complete Guide to AMP Page AdSense: Boost Mobile Ad Revenue by 48%
WordPress AdSense Optimization Guide: Plugin Selection & Configuration (2026 Edition)

Comments
Sign in with GitHub to leave a comment