The State of Node.js ORMs in 2026
TL;DR
Drizzle for new TypeScript projects; Prisma for teams prioritizing DX; Kysely for SQL purists. Drizzle ORM (~2M weekly downloads) had the fastest growth in 2025 — TypeScript-native, writes SQL you actually recognize, zero runtime overhead. Prisma (~5M downloads) is still the DX leader with best-in-class tooling, but the generated client and shadow database migration setup remains complex. TypeORM (~3M) is widely used but carries decorator/reflect-metadata baggage from 2018. Kysely (~500K) is for SQL experts who want TypeScript types on raw queries.
Key Takeaways
- Prisma: ~5M weekly downloads — best DX, schema-first, shadow DB migrations
- TypeORM: ~3M downloads — Active Record pattern, decorators, legacy-popular
- Drizzle ORM: ~2M downloads — fastest growth, TypeScript-first, SQL-like API
- Kysely: ~500K downloads — type-safe query builder (not full ORM), SQL expert tool
- Drizzle — grew 400% in 2025 downloads; on track to pass TypeORM
Prisma (DX Leader)
// prisma/schema.prisma — declarative schema language
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
profile Profile?
@@index([email])
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
tags Tag[] @relation("PostToTag")
createdAt DateTime @default(now())
@@index([authorId])
}
enum Role {
USER
ADMIN
}
// Prisma Client — full type safety from schema
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
log: ['query', 'error'], // SQL query logging
});
// Fully typed query — no manual types needed
const user = await prisma.user.findUnique({
where: { email: 'alice@example.com' },
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 5,
select: { id: true, title: true, createdAt: true }, // Only these fields
},
profile: true,
},
});
// user.posts[0].title — TypeScript knows the shape!
// user.posts[0].content — TypeScript error: not selected!
// Create with nested write
const newPost = await prisma.post.create({
data: {
title: 'Drizzle vs Prisma 2026',
content: '...',
author: { connect: { id: userId } }, // Connect existing user
tags: {
connectOrCreate: tags.map(tag => ({
where: { name: tag },
create: { name: tag },
})),
},
},
include: { author: { select: { name: true } } },
});
// Transaction
const [user, post] = await prisma.$transaction([
prisma.user.create({ data: { email: 'bob@example.com', name: 'Bob' } }),
prisma.post.create({ data: { title: 'First Post', authorId: 1 } }),
]);
// Raw SQL (escape hatch)
const result = await prisma.$queryRaw`
SELECT id, name, COUNT(posts.id) as post_count
FROM "User"
LEFT JOIN "Post" ON "Post"."authorId" = "User".id
GROUP BY "User".id
ORDER BY post_count DESC
LIMIT 10
`;
# Prisma workflow
npx prisma generate # Generate client from schema
npx prisma migrate dev # Create + apply migration (dev)
npx prisma migrate deploy # Apply migrations (production)
npx prisma studio # GUI database browser
npx prisma db pull # Introspect existing DB → schema
Drizzle ORM (TypeScript-Native SQL)
// Drizzle — TypeScript-first, SQL you recognize
// db/schema.ts
import {
pgTable, serial, varchar, integer, boolean,
timestamp, text, index,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 100 }).notNull(),
role: varchar('role', { length: 20 }).notNull().default('user'),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (table) => ({
emailIdx: index('email_idx').on(table.email),
}));
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
content: text('content'),
published: boolean('published').notNull().default(false),
authorId: integer('author_id').notNull().references(() => users.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
// Typed relations for joins
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));
// Drizzle — queries look like SQL
import { db } from './db';
import { users, posts } from './schema';
import { eq, and, desc, count, sql, like } from 'drizzle-orm';
// Select with join
const result = await db
.select({
userId: users.id,
userName: users.name,
postCount: count(posts.id),
})
.from(users)
.leftJoin(posts, eq(posts.authorId, users.id))
.where(eq(users.role, 'admin'))
.groupBy(users.id, users.name)
.orderBy(desc(count(posts.id)))
.limit(10);
// Insert
const [newUser] = await db
.insert(users)
.values({ email: 'alice@example.com', name: 'Alice' })
.returning();
// Update
await db
.update(posts)
.set({ published: true, title: sql`${posts.title} || ' (updated)'` })
.where(and(eq(posts.authorId, userId), eq(posts.published, false)));
// Delete
await db.delete(posts).where(eq(posts.id, postId));
// Relational queries (with relations defined)
const userWithPosts = await db.query.users.findFirst({
where: eq(users.email, 'alice@example.com'),
with: {
posts: {
where: eq(posts.published, true),
orderBy: [desc(posts.createdAt)],
limit: 5,
},
},
});
// userWithPosts.posts[0].title — fully typed!
// Drizzle — transactions
const result = await db.transaction(async (tx) => {
const [user] = await tx
.insert(users)
.values({ email: 'bob@example.com', name: 'Bob' })
.returning();
const [post] = await tx
.insert(posts)
.values({ title: 'First Post', authorId: user.id })
.returning();
return { user, post };
});
// Drizzle — dynamic queries (conditional)
import { SQL, ilike } from 'drizzle-orm';
function buildUserQuery(filters: { name?: string; role?: string }) {
const conditions: SQL[] = [];
if (filters.name) conditions.push(ilike(users.name, `%${filters.name}%`));
if (filters.role) conditions.push(eq(users.role, filters.role));
return db
.select()
.from(users)
.where(conditions.length > 0 ? and(...conditions) : undefined);
}
Kysely (Type-Safe Query Builder)
// Kysely — no magic, just typed SQL
import { Kysely, PostgresDialect } from 'kysely';
import { Pool } from 'pg';
// Define your DB schema types manually
interface Database {
users: { id: number; email: string; name: string; role: string };
posts: { id: number; title: string; authorId: number; published: boolean };
}
const db = new Kysely<Database>({
dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL }) }),
});
// Type-safe queries without an ORM layer
const users = await db
.selectFrom('users')
.innerJoin('posts', 'posts.authorId', 'users.id')
.select(['users.id', 'users.name', 'posts.title'])
.where('users.role', '=', 'admin')
.where('posts.published', '=', true)
.orderBy('users.name')
.execute();
Best for: SQL experts who want TypeScript types but no ORM abstraction layer.
ORM Comparison
| ORM | Downloads | Type Safety | Query Style | Migrations | Learning Curve |
|---|---|---|---|---|---|
| Prisma | 5M | ✅ Auto-generated | Fluent API | Prisma Migrate | Low |
| TypeORM | 3M | ⚠️ Decorators | Active Record | Auto/Manual | Medium |
| Drizzle | 2M | ✅ Inferred | SQL-like | drizzle-kit | Low-Medium |
| Kysely | 500K | ✅ Manual types | SQL-like | None (bring your own) | High (SQL expertise) |
| Sequelize | 2M | ⚠️ Partial | Active Record | Built-in | Medium |
When to Choose
| Scenario | Pick |
|---|---|
| New TypeScript project, best DX | Prisma |
| SQL-fluent team, TypeScript-first | Drizzle |
| Existing Prisma codebase | Stay with Prisma |
| Need to see/control SQL exactly | Drizzle or Kysely |
| Edge runtime (Cloudflare Workers) | Drizzle (Prisma has issues in edge) |
| SQL expert, no ORM abstraction | Kysely |
| Legacy codebase | TypeORM (if already in use) |
| Complex relational queries | Drizzle or Kysely |
| Rapid prototyping | Prisma |
The Drizzle Momentum Story
Drizzle grew from ~200K weekly downloads in early 2024 to ~2M in early 2026 — a 10x increase in 2 years. The inflection points:
- Cloudflare Workers support — Prisma struggled in edge runtimes; Drizzle worked natively
- PlanetScale killed free tier → Developers moved to Turso/libSQL → Drizzle had first-class Turso support
- SQL visibility — Developers were tired of ORMs hiding queries. Drizzle's "write SQL in TypeScript" resonated
- Bundle size — Drizzle core is ~3KB runtime (Prisma client generates a ~500KB bundle)
- drizzle-kit — Migration tooling that generates and shows SQL before applying
For any project starting in 2026, Drizzle and Prisma are the clear choices. TypeORM is maintained but in maintenance mode.
Compare ORM package health on PkgPulse.
See the live comparison
View prisma vs. drizzle on PkgPulse →