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);
}
TypeORM: The Established Standard (Showing Age)
TypeORM remains at ~3M downloads/week, kept alive by the enormous amount of Node.js code written between 2016-2022 that uses it. But the Active Record pattern and decorator-based configuration feel increasingly outdated against Drizzle and Prisma.
// TypeORM — decorator-heavy entity definition
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
name: string;
@Column({ default: 'user' })
role: string;
@CreateDateColumn()
createdAt: Date;
@OneToMany(() => Post, post => post.author)
posts: Post[];
}
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@ManyToOne(() => User, user => user.posts)
author: User;
}
// TypeORM — repository pattern queries
const userRepository = dataSource.getRepository(User);
// Find with relations
const user = await userRepository.findOne({
where: { email: 'alice@example.com' },
relations: { posts: true },
});
// Query builder for complex queries
const topAuthors = await userRepository
.createQueryBuilder('user')
.leftJoin('user.posts', 'post')
.select('user.id', 'id')
.addSelect('user.name', 'name')
.addSelect('COUNT(post.id)', 'postCount')
.groupBy('user.id')
.orderBy('postCount', 'DESC')
.limit(10)
.getRawMany();
TypeORM's issues in 2026:
- Requires
experimentalDecorators: truein tsconfig (being removed from TypeScript core) reflect-metadatapolyfill required (awkward in ESM)- Type inference for complex queries returns
anyfrequently - Migration tooling is less ergonomic than Prisma Migrate or drizzle-kit
- Active development has slowed
For new projects, choose Drizzle or Prisma. For existing TypeORM projects, the migration path to Drizzle is the most common exit.
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 |
The ORM Landscape in Context
The shift from TypeORM to Drizzle/Prisma reflects a broader maturation in how JavaScript developers think about databases. TypeORM's Active Record pattern, popularized by Ruby on Rails, made sense when JavaScript was a secondary language. As TypeScript became the primary language for serious Node.js development, the tradeoffs shifted.
Type safety quality varies dramatically across ORMs. Prisma's approach — generating a typed client from a schema file — gives the highest quality type inference with almost no manual type writing. Drizzle's approach — TypeScript-native schema definitions — gives excellent inference with slightly more explicit code. TypeORM's decorator approach gives partial inference that requires manual typing for complex queries. Kysely requires fully manual type definitions for the database schema.
Migration tooling is underappreciated. Schema evolution — adding columns, changing types, renaming tables — is a pain point in every ORM. Prisma Migrate creates a complete migration history with rollback support and handles most schema changes automatically. drizzle-kit generate inspects your schema diff and generates SQL migrations you can review before running. TypeORM's auto-migration (synchronize: true) is convenient in development but dangerous in production. Sequelize's migration system requires manual SQL in migration files.
Edge runtime compatibility changed the calculus. Before Cloudflare Workers became mainstream, ORM bundle size was a minor concern. Now it's a primary decision factor for APIs that need global distribution. Drizzle (3KB runtime) works in any edge environment. Prisma (500KB+ with the Rust query engine) doesn't fit in Cloudflare Workers without using Prisma Accelerate as a proxy. This single constraint drives many teams from Prisma to Drizzle for greenfield projects.
The "connection pooling" consideration. Serverless deployments (Vercel Functions, AWS Lambda, Cloudflare Workers) create and destroy database connections on every request. Traditional PostgreSQL/MySQL have limits of 100-500 concurrent connections. Without connection pooling, a spike in serverless function invocations can exhaust database connections. All major ORMs work with external poolers (PgBouncer, Neon's pooler, Supabase pooler), but Prisma and Drizzle have the best documentation for this setup.
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 |
What About Sequelize?
Sequelize (~2M downloads/week) is not dead, but it's in a similar position to TypeORM — maintained but not evolving. Sequelize 7 (in beta as of 2026) adds TypeScript-native support and drops many legacy patterns. But it's playing catch-up with Drizzle's native TypeScript design.
Sequelize is most common in Node.js applications that were built before 2020 and have accumulated a large codebase on it. The migration cost is real, and for stable applications not adding new features heavily, there's no compelling reason to migrate. But for new projects, Sequelize doesn't make the shortlist — the TypeScript story is too cumbersome compared to Drizzle.
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.
The Prisma vs Drizzle Decision in Practice
The "Prisma vs Drizzle" question is asked on every TypeScript backend project in 2026. Here's the practical breakdown:
Choose Prisma when: your team is small, you're moving fast, and you want schema-to-types-to-queries to just work. Prisma's auto-generated client means you never write a type definition for your data models — they flow directly from the schema. Prisma Studio (a visual database browser) is genuinely useful for non-technical team members to inspect data. The npx prisma migrate dev workflow handles most migration scenarios well. If you're building a startup product and want to focus on features, not DB tooling, Prisma wins.
Choose Drizzle when: SQL visibility matters, you're deploying to edge runtimes, or your team has strong SQL skills. Drizzle's query builder writes SQL that's immediately readable — there's no "what does Prisma generate for this?" mystery. The 3KB runtime vs Prisma's 500KB generated client is a real consideration for edge deployments. drizzle-kit shows you the exact SQL migration before running it. For teams where developers want to see and control the database queries, Drizzle gives you that without sacrificing type safety.
The Prisma bundle size problem is worth calling out: Prisma's generated client includes the query engine (a Rust binary) and generated TypeScript code totaling ~500KB-1MB depending on schema size. This is fine for serverful deployments but causes real issues with Cloudflare Workers (1MB limit), Vercel Edge Functions, and AWS Lambda cold starts. Drizzle has no equivalent overhead.
Migration from Prisma to Drizzle is increasingly common as teams hit edge runtime limits. The process: use drizzle-kit introspect to generate Drizzle schema from your existing database (skipping the Prisma schema step), then migrate queries one by one. Most teams report 1-2 weeks for a medium-sized app migration.
Edge Runtime Support: The Constraint That Changed the Market
The ORM landscape in 2026 can't be understood without the edge runtime constraint. Cloudflare Workers, Vercel Edge Functions, and Deno Deploy all run a restricted JavaScript environment: no Node.js built-ins, no binary native modules, TCP socket limitations.
Prisma's generated client uses native binaries for database communication. It also historically required a shadow database for migrations. Both of these were incompatible with edge runtimes. For developers building applications where the API layer needs to run on Cloudflare Workers for global latency, Prisma was simply off the table.
Drizzle's architecture sidesteps this entirely. It uses HTTP-based database drivers rather than direct TCP connections, and the schema/query layer is pure JavaScript with no native bindings. Drizzle works natively with:
- Neon via
@neondatabase/serverless(WebSocket-based driver) - Turso/libSQL via
@libsql/client - PlanetScale (before it pivoted) via the HTTP API
- Supabase via the REST/HTTP connector
This edge runtime support directly drove Drizzle's 10x growth. Every developer building on Cloudflare Workers, Vercel Edge, or the App Router's server components with edge runtime learned quickly that Drizzle was the ORM that worked.
Prisma responded with Prisma Accelerate — a connection pooling proxy that handles the edge compatibility issue externally. With Accelerate, Prisma works in edge environments, but it requires running Prisma Accelerate as an intermediary (Prisma hosts it; self-hosting is possible but complex). Teams that need Prisma's DX in edge contexts now have a path, but it adds a managed dependency.
Prisma to Drizzle Migration: What's Involved
Many teams started with Prisma and are evaluating Drizzle as their stack evolves. The migration is non-trivial but well-documented. Here's the practical breakdown:
Schema migration: Prisma's .prisma schema doesn't map 1:1 to Drizzle's TypeScript schema definitions. You'll rewrite the schema. The upside: you write it once and understand every column; the downside: a large Prisma schema (50+ models) is a significant rewrite.
Query rewriting: Prisma's fluent API (findUnique, findMany, create) becomes Drizzle's SQL-like API (db.select().from().where() or db.query.*). The logic is equivalent but the syntax is different.
Migrations: Prisma Migrate generates SQL migration files and applies them. Drizzle Kit does the same. The transition requires generating a Drizzle migration from the current database state (using drizzle-kit introspect) rather than starting from scratch.
What you gain: Visible SQL, smaller bundle size (~3KB vs ~500KB for Prisma client), edge runtime support, and direct SQL control when you need it. What you lose: Prisma Studio (GUI database browser), Prisma Accelerate if you're already using it, and the onboarding story — Prisma's documentation is still more beginner-friendly.
The migration is worth it for teams hitting Prisma's limitations. For teams happy with Prisma, there's no compelling reason to switch.
Performance: N+1 Queries and What ORMs Don't Show You
Every ORM comparison includes benchmark numbers, but the performance issue that bites production applications most often isn't throughput — it's the N+1 query problem.
An N+1 query happens when fetching a list of 100 users executes 1 query for the list, then 100 individual queries for each user's related data. For 100 users, that's 101 queries instead of 2.
All four ORMs handle N+1 differently:
Prisma: include with nested selects batches related data automatically. Prisma's dataloader batches multiple findUnique calls into a single query. Well-protected against N+1 for common patterns.
Drizzle: Relational queries (db.query.users.findMany({ with: { posts: true } })) handle N+1 correctly. The raw query API (.select().from()) does not — you're responsible for writing the join yourself.
TypeORM: The @Lazy decorator and relations config allow either lazy loading (N+1 risk) or eager loading. Most N+1 issues in TypeORM codebases come from lazy-loading relations in loop contexts.
Kysely: You're writing SQL directly. N+1 is only possible if you choose to write it that way — Kysely users are generally aware of query count.
The ORM you choose affects not just developer experience but the database load patterns your application creates. Teams switching from TypeORM to Drizzle frequently discover and eliminate N+1 queries in the process, because writing explicit joins makes the query count visible.
Looking Ahead: ORM Trends for 2026-2027
Drizzle will likely surpass TypeORM in downloads by mid-2026, continuing its growth trajectory. The combination of edge-runtime compatibility, TypeScript-native design, and SQL transparency has driven its adoption among developers building modern stacks.
Prisma Accelerate (a connection pooler and caching layer as a service) is Prisma's strategic response to edge limitations. Rather than redesigning the client for edge runtimes, Prisma routes queries through Accelerate which handles the database connection. This keeps Prisma viable for edge deployments but adds a dependency on Prisma's infrastructure. Some teams see this as pragmatic; others prefer Drizzle's zero-infrastructure approach.
The database diversity challenge. Neon, Turso, PlanetScale (revived), Supabase, and Cloudflare D1 have fragmented the database landscape. ORM compatibility with these providers — particularly their edge-compatible drivers — increasingly influences ORM choice. Drizzle was fastest to add first-class support for newer databases (Turso/libSQL, Cloudflare D1, Bun SQLite); Prisma's coverage is broader but lags on cutting-edge databases.
AI-assisted query building is an emerging consideration. AI coding assistants (Cursor, Copilot) have extensive training data for Prisma (older, more popular). Drizzle's SQL-like syntax is also well-represented in training data. TypeORM's decorator syntax generates less accurate AI suggestions. Teams using AI tools heavily report better results with Prisma and Drizzle.
What about Mongoose? Mongoose (~3M weekly downloads) is the ORM for MongoDB and occupies a different category — it's SQL-ORM alternatives when your database is document-oriented. Mongoose is stable, widely used, and still the standard for MongoDB + Node.js. But the broader Node.js ORM conversation in 2026 is SQL-focused, reflecting the industry's renewed interest in relational databases for reliability and consistency.
Testing with ORMs deserves a mention. Prisma's recommended testing approach — using a real database with a dedicated test schema — requires more setup but produces reliable tests. Drizzle's approach works well with both real databases and SQLite in-memory databases for fast unit tests. TypeORM's testing story has historically been more complex, requiring either database mocking or full test database setup. For teams that prioritize fast, reliable tests, Drizzle's flexibility here is an advantage.
Prisma 5 and Prisma 6's new features include better support for composite primary keys, improved performance for large schemas, and the TypedSQL feature — a way to write raw SQL queries in .sql files and have Prisma generate TypeScript types for them automatically. This narrows the gap with Drizzle's SQL visibility advantage. The ORM landscape continues to evolve, but the Prisma vs Drizzle tension will define Node.js database tooling for the next 2-3 years.
ORM Migration Strategies: When and How to Switch
The decision to migrate an existing codebase from one ORM to another is one of the most impactful — and risky — architectural decisions a backend team makes. Here's how teams approach it successfully.
From TypeORM to Drizzle (most common migration in 2026). TypeORM's decorator-based approach means migrations are scattered across model class definitions rather than centralized. The migration path: use drizzle-kit introspect to generate a Drizzle schema from your existing database (bypassing TypeORM schemas entirely), then replace queries module by module. Teams report 3-6 weeks for a medium-sized API (50-100 model classes). The key risk: TypeORM allows some patterns (like @OneToMany without explicit foreign keys) that require explicit handling in Drizzle.
From Prisma to Drizzle. Driven primarily by edge runtime requirements or bundle size concerns. The process is similar: drizzle-kit introspect for schema, then query-by-query migration. Prisma's prisma.$queryRaw callsites need particular attention — Drizzle's SQL template literal (sql\SELECT...``) is the equivalent. Teams with heavy Prisma middleware usage need to reimplement that logic in Drizzle's query lifecycle hooks.
The incremental approach. For large codebases, a full migration is impractical. Many teams run Prisma and Drizzle in parallel during the transition — new features use Drizzle, legacy code keeps Prisma until those modules are touched. Both ORMs can share a connection pool (using the underlying pg or neon driver), so the dual-ORM approach is operationally feasible.
When not to migrate. If your application is stable, not hitting edge runtime limits, and your team is productive with the current ORM, the migration cost rarely pays off. ORM migrations are justified by concrete constraints: hitting Prisma's bundle limits for edge functions, needing Drizzle's SQL visibility for a complex query optimization task, or moving away from TypeORM's maintenance trajectory for a long-lived application.
Compare ORM package health on PkgPulse.
The ORM decision in 2026 is ultimately a bet on which abstraction level you trust: Prisma bets you want schema-driven DX with generated clients, Drizzle bets you want TypeScript-native SQL with maximum visibility, Kysely bets you want pure type-safe SQL without any ORM layer, and TypeORM serves those who need backward compatibility with a large existing codebase. All are valid choices for the right context.
See also: Drizzle ORM vs TypeORM and Drizzle ORM vs Prisma, ORM Packages Compared (2026).
See the live comparison
View prisma vs. drizzle on PkgPulse →