Skip to main content

The State of Node.js ORMs in 2026

·PkgPulse Team

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

ORMDownloadsType SafetyQuery StyleMigrationsLearning Curve
Prisma5M✅ Auto-generatedFluent APIPrisma MigrateLow
TypeORM3M⚠️ DecoratorsActive RecordAuto/ManualMedium
Drizzle2M✅ InferredSQL-likedrizzle-kitLow-Medium
Kysely500K✅ Manual typesSQL-likeNone (bring your own)High (SQL expertise)
Sequelize2M⚠️ PartialActive RecordBuilt-inMedium

When to Choose

ScenarioPick
New TypeScript project, best DXPrisma
SQL-fluent team, TypeScript-firstDrizzle
Existing Prisma codebaseStay with Prisma
Need to see/control SQL exactlyDrizzle or Kysely
Edge runtime (Cloudflare Workers)Drizzle (Prisma has issues in edge)
SQL expert, no ORM abstractionKysely
Legacy codebaseTypeORM (if already in use)
Complex relational queriesDrizzle or Kysely
Rapid prototypingPrisma

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:

  1. Cloudflare Workers support — Prisma struggled in edge runtimes; Drizzle worked natively
  2. PlanetScale killed free tier → Developers moved to Turso/libSQL → Drizzle had first-class Turso support
  3. SQL visibility — Developers were tired of ORMs hiding queries. Drizzle's "write SQL in TypeScript" resonated
  4. Bundle size — Drizzle core is ~3KB runtime (Prisma client generates a ~500KB bundle)
  5. 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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.