TypeScript ORM Comparison | Prisma vs Drizzle vs TypeORM vs Kysely

TypeScript ORM Comparison | Prisma vs Drizzle vs TypeORM vs Kysely

이 글의 핵심

Prisma, Drizzle, TypeORM, and Kysely each take a different approach to database access in TypeScript. This comparison covers type safety, query ergonomics, performance, bundle size, and migration workflows to help you pick the right tool.

The Four Options

PrismaDrizzleTypeORMKysely
Type safetyExcellentExcellentGoodExcellent
Query styleObject APISQL-likeActive RecordSQL builder
Bundle sizeLarge (~20MB binary)Tiny (~50KB)MediumTiny (~20KB)
Edge/serverlessLimitedNativeNoYes
MigrationsSchema → MigrationCode-firstDecoratorsManual
Raw SQLSupportedNaturalSupportedCore
Learning curveLowMediumMediumLow-Medium
EcosystemLargeGrowingMatureSmall

1. Prisma

Prisma uses a schema file to generate types and a query client.

// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
}
# Generate client + migrate
npx prisma migrate dev --name add-users
npx prisma generate
import { PrismaClient } from '@prisma/client'
const db = new PrismaClient()

// Fully typed queries
const users = await db.user.findMany({
  where: { email: { contains: '@example.com' } },
  include: { posts: { where: { published: true } } },
  orderBy: { createdAt: 'desc' },
  take: 10,
})
// users: (User & { posts: Post[] })[]

// Create with relations
const user = await db.user.create({
  data: {
    email: '[email protected]',
    name: 'Alice',
    posts: {
      create: [
        { title: 'First Post', content: 'Hello world', published: true }
      ]
    }
  },
  include: { posts: true }
})

// Transaction
await db.$transaction(async (tx) => {
  const user = await tx.user.create({ data: { email: '[email protected]', name: 'Bob' } })
  await tx.post.create({ data: { title: 'Welcome', content: '...', authorId: user.id } })
})

Prisma strengths:

  • Best-in-class DX — intuitive, auto-complete everywhere
  • Schema → types → client in one flow
  • Prisma Studio (visual DB browser)
  • Strong documentation

Prisma weaknesses:

  • Large binary (~20MB Rust query engine)
  • Doesn’t work in Cloudflare Workers without Accelerate
  • Complex raw SQL integration
  • Slower cold starts in serverless

2. Drizzle ORM

Drizzle defines schema in TypeScript — no separate schema file.

// db/schema.ts
import { pgTable, text, boolean, timestamp } from 'drizzle-orm/pg-core'
import { relations } from 'drizzle-orm'

export const users = pgTable('users', {
  id: text('id').primaryKey().default(sql`gen_random_uuid()`),
  email: text('email').notNull().unique(),
  name: text('name').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})

export const posts = pgTable('posts', {
  id: text('id').primaryKey().default(sql`gen_random_uuid()`),
  title: text('title').notNull(),
  content: text('content').notNull(),
  published: boolean('published').default(false).notNull(),
  authorId: text('author_id').notNull().references(() => users.id),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}))

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, { fields: [posts.authorId], references: [users.id] }),
}))
import { drizzle } from 'drizzle-orm/node-postgres'
import { eq, like, desc } from 'drizzle-orm'
import * as schema from './schema'

const db = drizzle(pool, { schema })

// SQL-like queries
const users = await db
  .select()
  .from(schema.users)
  .where(like(schema.users.email, '%@example.com'))
  .orderBy(desc(schema.users.createdAt))
  .limit(10)

// Relational query (auto-joins)
const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: { where: (posts, { eq }) => eq(posts.published, true) }
  },
  limit: 10,
})

// Insert
const [user] = await db.insert(schema.users)
  .values({ email: '[email protected]', name: 'Alice' })
  .returning()

// Transaction
await db.transaction(async (tx) => {
  const [user] = await tx.insert(schema.users)
    .values({ email: '[email protected]', name: 'Bob' })
    .returning()
  await tx.insert(schema.posts)
    .values({ title: 'Welcome', content: '...', authorId: user.id })
})

Drizzle strengths:

  • Tiny bundle — works in Cloudflare Workers, Deno, Bun
  • SQL-like API — easy to understand what query is generated
  • Fast — minimal abstraction overhead
  • TypeScript-first schema

Drizzle weaknesses:

  • Less beginner-friendly than Prisma
  • Smaller ecosystem/community
  • No GUI (Drizzle Studio is newer)

3. TypeORM

TypeORM uses decorators on classes.

// entity/User.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn } from 'typeorm'
import { Post } from './Post'

@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column({ unique: true })
  email: string

  @Column()
  name: string

  @OneToMany(() => Post, post => post.author)
  posts: Post[]

  @CreateDateColumn()
  createdAt: Date
}

// entity/Post.ts
@Entity('posts')
export class Post {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column()
  title: string

  @Column('text')
  content: string

  @Column({ default: false })
  published: boolean

  @ManyToOne(() => User, user => user.posts)
  author: User

  @Column()
  authorId: string
}
import { DataSource } from 'typeorm'

const dataSource = new DataSource({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  entities: [User, Post],
  synchronize: false,  // use migrations in production!
  logging: false,
})

await dataSource.initialize()

const userRepo = dataSource.getRepository(User)

// Find with relations
const users = await userRepo.find({
  where: { email: Like('%@example.com') },
  relations: { posts: true },
  order: { createdAt: 'DESC' },
  take: 10,
})

// Query builder (more complex queries)
const result = await userRepo.createQueryBuilder('user')
  .leftJoinAndSelect('user.posts', 'post')
  .where('post.published = :published', { published: true })
  .orderBy('user.createdAt', 'DESC')
  .getMany()

TypeORM strengths:

  • Mature, battle-tested
  • Active Record + Data Mapper patterns
  • Works with many databases

TypeORM weaknesses:

  • Weaker TypeScript inference than Prisma/Drizzle
  • Decorator API can be complex
  • Type inference for relations is unreliable
  • Not recommended for new projects in 2026

4. Kysely

Kysely is a type-safe SQL query builder — no schema file, SQL-first.

import { Kysely, PostgresDialect } from 'kysely'
import { Pool } from 'pg'

// Define database types manually
interface Database {
  users: {
    id: string
    email: string
    name: string
    created_at: Date
  }
  posts: {
    id: string
    title: string
    content: string
    published: boolean
    author_id: string
    created_at: Date
  }
}

const db = new Kysely<Database>({
  dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL }) }),
})

// Fully typed SQL queries
const users = await db
  .selectFrom('users')
  .select(['id', 'email', 'name'])
  .where('email', 'like', '%@example.com')
  .orderBy('created_at', 'desc')
  .limit(10)
  .execute()

// Join
const usersWithPosts = await db
  .selectFrom('users')
  .innerJoin('posts', 'posts.author_id', 'users.id')
  .select([
    'users.id',
    'users.name',
    'posts.title',
    'posts.published',
  ])
  .where('posts.published', '=', true)
  .execute()

// Insert
const user = await db
  .insertInto('users')
  .values({ id: randomUUID(), email: '[email protected]', name: 'Alice', created_at: new Date() })
  .returning(['id', 'email'])
  .executeTakeFirstOrThrow()

// Transaction
await db.transaction().execute(async (trx) => {
  const user = await trx.insertInto('users')
    .values({ /* ... */ })
    .returningAll()
    .executeTakeFirstOrThrow()

  await trx.insertInto('posts')
    .values({ author_id: user.id, /* ... */ })
    .execute()
})

Kysely strengths:

  • Complete SQL control — no magic
  • Excellent TypeScript inference
  • Tiny bundle — edge/serverless friendly
  • SQL is what you get — no surprises

Kysely weaknesses:

  • Must define types manually (or use codegen)
  • No built-in migration system
  • More verbose than Prisma for simple queries
  • Less beginner-friendly

Decision Guide

New project, team of developers, CRUD-heavy app?
  → Prisma (best DX, docs, onboarding)

Edge/serverless (Cloudflare Workers, Deno Deploy)?
  → Drizzle (tiny bundle, native edge support)

Need fine-grained SQL control, complex queries?
  → Drizzle or Kysely

Existing TypeORM project?
  → Keep TypeORM (migration cost not worth it unless you have issues)

Library or tool needing zero dependencies?
  → Kysely (SQL-first, typed, tiny)

Performance Comparison

Benchmarks on PostgreSQL with 10K rows, 100 concurrent requests:

OperationPrismaDrizzleTypeORMKyselyRaw SQL
Simple SELECT3.2ms1.1ms2.8ms1.0ms0.8ms
JOIN query5.1ms1.8ms4.2ms1.6ms1.2ms
INSERT + return2.8ms1.2ms2.5ms1.1ms0.9ms
Cold start+80ms+5ms+40ms+3ms

Approximate values. Real-world differences depend on query complexity and infrastructure.


Migration Strategy

ToolApproach
PrismaEdit schema.prismaprisma migrate dev
DrizzleEdit schema files → drizzle-kit generatedrizzle-kit migrate
TypeORMEdit entities → typeorm migration:generate
KyselyWrite SQL migration files manually

Key Takeaways

  • Prisma: Best DX and docs — choose for most team projects in 2026
  • Drizzle: Best for edge runtimes, complex SQL, zero bundle overhead
  • TypeORM: Avoid for new projects — type inference is weaker
  • Kysely: Best when you want full SQL control with TypeScript safety
  • All four support PostgreSQL, MySQL, SQLite — pick the right tool for your constraints, not the most popular one