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
| Prisma | Drizzle | TypeORM | Kysely | |
|---|---|---|---|---|
| Type safety | Excellent | Excellent | Good | Excellent |
| Query style | Object API | SQL-like | Active Record | SQL builder |
| Bundle size | Large (~20MB binary) | Tiny (~50KB) | Medium | Tiny (~20KB) |
| Edge/serverless | Limited | Native | No | Yes |
| Migrations | Schema → Migration | Code-first | Decorators | Manual |
| Raw SQL | Supported | Natural | Supported | Core |
| Learning curve | Low | Medium | Medium | Low-Medium |
| Ecosystem | Large | Growing | Mature | Small |
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: 'alice@example.com',
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: 'bob@example.com', 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: 'alice@example.com', name: 'Alice' })
.returning()
// Transaction
await db.transaction(async (tx) => {
const [user] = await tx.insert(schema.users)
.values({ email: 'bob@example.com', 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: 'alice@example.com', 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:
| Operation | Prisma | Drizzle | TypeORM | Kysely | Raw SQL |
|---|---|---|---|---|---|
| Simple SELECT | 3.2ms | 1.1ms | 2.8ms | 1.0ms | 0.8ms |
| JOIN query | 5.1ms | 1.8ms | 4.2ms | 1.6ms | 1.2ms |
| INSERT + return | 2.8ms | 1.2ms | 2.5ms | 1.1ms | 0.9ms |
| Cold start | +80ms | +5ms | +40ms | +3ms | — |
Approximate values. Real-world differences depend on query complexity and infrastructure.
Migration Strategy
| Tool | Approach |
|---|---|
| Prisma | Edit schema.prisma → prisma migrate dev |
| Drizzle | Edit schema files → drizzle-kit generate → drizzle-kit migrate |
| TypeORM | Edit entities → typeorm migration:generate |
| Kysely | Write 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