Skip to content

Split View: TypeScript ORM·쿼리 빌더 2026 — Drizzle·Kysely·Prisma·postgres.js 심층 비교 (SQL에 얼마나 가까워야 하는가)

|

TypeScript ORM·쿼리 빌더 2026 — Drizzle·Kysely·Prisma·postgres.js 심층 비교 (SQL에 얼마나 가까워야 하는가)

프롤로그 — "Prisma 또는 raw SQL"의 시대는 끝났다

2026년에도 신규 TypeScript 프로젝트의 첫 번째 회의에서 빠지지 않는 질문이 있다.

"DB 접근, 뭐 쓸 거예요?"

2022년만 해도 답은 보통 두 개였다. Prisma(가장 유명, 풀 ORM) 아니면 raw pg/mysql2 (직접 SQL, 타입은 알아서). 사이에는 TypeORM·Sequelize 같은 클래스 기반 ORM이 끼어 있었지만, TypeScript 친화도와 마이그레이션 경험 때문에 신규 프로젝트에서는 점점 안 쓰였다.

2026년은 다르다. 두 개의 새로운 강자가 자리를 굳혔다.

  • Drizzle ORM — "SQL에 가장 가까운" 헤드리스 ORM. 스키마를 TS 코드로 선언하고, 쿼리는 SQL 한 줄 한 줄에 1:1 대응되게 짠다. 번들 크기와 엣지 런타임에 강하다. 2026년 5월 기준 GitHub 스타 31k+, 주간 npm 다운로드 2M+.
  • Kysely — "스키마 없는 순수 쿼리 빌더". 어떤 DB 클라이언트(pg·mysql2·better-sqlite3)도 받아서 타입 안전한 쿼리 빌더로 감싼다. 마이그레이션·스키마 관리는 너의 일이다. GitHub 스타 12k+.

여기에 Prisma가 새로 등장한 두 트럼프 카드.

  • Prisma 엔진 재작성 — Prisma 6 이후 기존 Rust 기반 query engine이 TypeScript 네이티브 + Go 엔진 조합으로 단계적으로 교체됐다. 차가운 시작 지연이 줄고, 엣지 런타임 호환성이 정상화됐고, 번들 크기가 의미 있게 줄었다.
  • Prisma Postgres — Prisma 사가 직접 운영하는 매니지드 Postgres. unikernel·Bare metal 위의 분리 컴퓨트·스토리지 아키텍처로, free tier가 의외로 후하고 cold start가 빠르다.

그리고 모두를 흔드는 한 갈래.

  • postgres.js·pg·mysql2 직접 쓰기 — tagged template literal과 TypeScript의 satisfies·as const만 잘 쓰면, ORM 없이도 충분히 타입 안전하다는 흐름. "쿼리 빌더 자체가 추상화 과잉" 진영.

이 글은 2026년 5월 기준으로 이 도구들을 직접 비교한다. 추상화 정도, 마이그레이션, 엣지, 번들, escape hatch, 멀티 DB의 6축으로. 그리고 같은 쿼리를 네 가지 도구로 나란히 짜본다.


1장 · 풍경 — 도구 지도

먼저 도구를 분류한다. 모든 게 같은 자리에 있지 않다.

분류도구한 줄 요약
풀 ORM (Active Record·Data Mapper)Prisma·MikroORM·TypeORM·Sequelize모델·관계·페치·캐시까지 챙긴다
헤드리스 ORMDrizzle스키마는 코드, 쿼리는 SQL 1:1
쿼리 빌더Kysely·Knex스키마 없이 타입 안전 SQL 합성
Raw 클라이언트 + 타입 헬퍼postgres.js·pg·mysql2 + Zodtagged template + 런타임 검증
관계형 DSLEdgeQL(EdgeDB)·SurrealQLDB가 자체 쿼리 언어

이 글의 초점은 굵게 표시한 4그룹의 대표 — Prisma·Drizzle·Kysely·postgres.js — 다. MikroORM·TypeORM은 마지막 장에서 짧게 다룬다.

왜 이 4개인가

  • Prisma — 가장 유명하고, 신규 풀스택 프로젝트의 기본값에 가깝다. 2026년의 새 엔진과 Postgres 서비스를 합치면 다시 1군이다.
  • Drizzle — 2024~2026 사이 가장 빠르게 자란 도구. Vercel 공식 가이드, Hono·Cloudflare Workers 권장 스택, T3 stack 기본 선택지 중 하나.
  • Kysely — Prisma에서 도망 온 팀이 가장 먼저 만나는 도구. 마이그레이션 후기에 자주 등장한다.
  • postgres.js — Porsager가 만든 빠르고 작은 Postgres 클라이언트. Bun·Cloudflare Workers와 궁합이 좋다. ORM 없는 진영의 사실상 표준.

2장 · 추상화 스펙트럼 — 어디까지 감출 것인가

데이터 접근 라이브러리는 "DB와 코드 사이에 얼마나 두꺼운 층을 둘지"의 선택이다. 왼쪽이 SQL에 가깝고, 오른쪽이 객체에 가깝다.

SQL                                                              객체
 |                                                                |
postgres.js   --   Kysely   --   Drizzle   --   Prisma   --   MikroORM/TypeORM
(raw)             (빌더)         (헤드리스 ORM)   (풀 ORM)        (Active Record)

추상화가 두꺼울수록 좋은가? 단언컨대 아니다. 트레이드오프다.

  • 얇은 쪽 장점: SQL 그대로의 표현력, 성능 튜닝의 직진성, 학습/디버그 코스트가 결국 SQL 한 가지로 수렴.
  • 두꺼운 쪽 장점: 작성 속도, 도메인 객체와 직접 매핑, 관계·N+1 자동 처리, IDE 자동완성의 풍성함.
  • 얇은 쪽 단점: 관계 페치를 직접 짜야 함, 도메인 객체 매핑은 별도 작업.
  • 두꺼운 쪽 단점: 추상화가 부족할 때(window function·CTE·복잡한 join) escape hatch가 어색하고, 번들·콜드 스타트·런타임 호환성 모두 비용.

2026년의 분위기는 "SQL에 가깝게, 단 타입은 안전하게" 쪽으로 기울었다. Drizzle·Kysely·postgres.js의 성장이 그 증거다. 다만 Prisma 6의 엔진 재작성으로 두꺼운 쪽도 가벼워져서, "ORM이라서 무거워요" 논리는 옛말이 되어 가고 있다.


3장 · Drizzle — "TypeScript로 쓰는 SQL"

Drizzle은 2026년 신규 프로젝트에서 가장 많이 선택되는 데이터베이스 도구 중 하나다. 핵심 철학 3가지.

  1. 스키마는 TS 코드schema.ts에서 테이블·컬럼·관계를 선언한다. DB가 진실의 원천이 아니라, TS 파일이 진실의 원천이다.
  2. 쿼리는 SQL 1:1db.select().from(users).where(eq(users.email, '...')) 같은 식. SQL을 아는 사람이 보면 무엇으로 컴파일될지 즉시 알 수 있다.
  3. 헤드리스 + 작음 — 런타임 의존성이 거의 없다. Cloudflare Workers·Vercel Edge·Deno Deploy에 그대로 올라간다. 번들 크기는 압축 기준 7~15KB 수준.

Drizzle 스키마

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

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

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  authorId: integer('author_id').notNull().references(() => users.id),
  title: text('title').notNull(),
  body: text('body').notNull(),
})

Drizzle Kit으로 마이그레이션

# drizzle.config.ts 작성 후
npx drizzle-kit generate   # 스키마 변경에서 SQL 마이그레이션 생성
npx drizzle-kit migrate    # 실제 DB에 적용
npx drizzle-kit studio     # 로컬 GUI

Drizzle의 강점

  • SQL escape hatch가 자연스러움sql 템플릿 태그로 임의 SQL을 끼워 넣고, 그 부분만 타입을 직접 정의할 수 있다.
  • 엣지 호환성 — 모든 어댑터(drizzle-orm/postgres-js, drizzle-orm/neon-http, drizzle-orm/d1)가 엣지 런타임에서 동작.
  • 여러 DB 지원 — PostgreSQL·MySQL·SQLite·D1·LibSQL·Neon·Planetscale·Vercel Postgres·Bun SQLite. 변환 비용이 거의 없다.

Drizzle의 약점

  • 관계 쿼리(Drizzle Relations API)는 발전 중 — 관계로 nested 쿼리를 짤 때 Prisma만큼 매끄럽진 않다. 다만 2025년 v2 출시로 거의 따라잡았다.
  • 마이그레이션 충돌 처리는 수동 — 협업 팀에서 마이그레이션 파일 충돌 시 손으로 머지해야 한다.
  • 러닝 커브 — SQL을 잘 모르면 Prisma보다 친절하지 않다.

4장 · Kysely — "스키마 없는 순수 쿼리 빌더"

Kysely는 다른 길이다. 스키마를 강제하지 않는다. 너의 DB 스키마는 너의 일이고(SQL 파일·Atlas·Sqitch·Liquibase 무엇이든), Kysely는 그 위에 타입 안전 쿼리 빌더만 제공한다.

// db/types.ts — 너의 DB 스키마와 매칭되는 TS 타입
import type { ColumnType, Generated } from 'kysely'

export interface Database {
  user: UserTable
  post: PostTable
}

export interface UserTable {
  id: Generated<number>
  email: string
  name: string
  created_at: ColumnType<Date, string | undefined, never>
}

export interface PostTable {
  id: Generated<number>
  author_id: number
  title: string
  body: string
}

Kysely의 핵심 트릭은 위 Database 인터페이스를 제네릭으로 받아서, 모든 쿼리에서 컬럼 이름·타입을 자동 추론하는 것이다.

kysely-codegen으로 타입 자동 생성

스키마 인터페이스를 손으로 쓰지 않으려면 kysely-codegen을 쓴다. 라이브 DB를 인트로스펙션해서 위 같은 TS 타입을 생성한다.

npx kysely-codegen --url postgres://user:pw@localhost/db --out-file db/types.ts

Kysely의 강점

  • SQL 그대로의 표현력 — window function·CTE·json_agg·UNION ALL 등 SQL 거의 전부가 1:1 표현 가능.
  • DB·마이그레이션 분리 — 마이그레이션 도구는 따로 골라 쓴다. Atlas·Sqitch·dbmate·Flyway 무엇이든.
  • 소형 번들 — 핵심 빌더만 가져오면 압축 약 14KB.
  • 런타임 의존성 최소 — Cloudflare Workers·Bun·Deno에서 무난.

Kysely의 약점

  • 스키마·타입의 진실 일치는 너의 일 — 마이그레이션 후 kysely-codegen을 실행하지 않으면 타입이 거짓말을 한다. CI에 묶거나, 마이그레이션 파일 후크에 자동화 필요.
  • 관계 페치는 명시적 — N+1·JOIN·json_agg 패턴을 직접 짠다. Prisma·Drizzle 만큼 짧지 않다.
  • 에코시스템 작음 — 플러그인·튜토리얼·서드파티 어댑터가 다른 두 도구보다 적다.

5장 · Prisma — 새 엔진과 Prisma Postgres

Prisma는 2024~2025 사이 두 번의 큰 변화가 있었다.

5.1 엔진 재작성 — Rust에서 TS 네이티브 + Go로

Prisma 5 시대의 query engine은 Rust로 작성된 별도 프로세스(또는 WASM)였다. 콜드 스타트가 느리고, 엣지 런타임 호환성이 어색했다. Prisma 6 이후 단계적 재작성으로 다음이 바뀌었다.

  • TypeScript 네이티브 클라이언트 — 쿼리 컴파일러 다수가 TS로 옮겨졌다. 클라이언트 코드 안에서 직접 SQL을 만든다.
  • Go 기반 lightweight 엔진 — Prisma Accelerate·Pulse 같은 매니지드 서비스 측에서 동작.
  • 번들 크기 감소 — 클라이언트 사이즈가 의미 있게 줄었다. 정확한 수치는 setup 별로 다르니 Prisma 자체 벤치마크와 릴리즈 노트를 참고하라.
  • 엣지 호환성 정상화 — Vercel Edge·Cloudflare Workers에서 직접 동작하는 어댑터가 안정화됐다.

5.2 TypedSQL — Prisma 안에서 raw SQL을 타입 안전하게

Prisma의 약점은 항상 "복잡한 SQL을 못 짠다"였다. TypedSQL은 그 약점을 직접 친다.

-- prisma/sql/findActiveUsers.sql
SELECT id, email, name
FROM "User"
WHERE last_seen_at > $1
ORDER BY last_seen_at DESC
LIMIT $2;
import { PrismaClient } from '@prisma/client'
import { findActiveUsers } from '@prisma/client/sql'

const prisma = new PrismaClient()
const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
const users = await prisma.$queryRawTyped(findActiveUsers(since, 50))
//    ^ users: Array of typed rows, inferred from the SQL parameters

.sql 파일이 빌드 타임에 검사되고, 파라미터·결과 타입이 자동 생성된다. SQL을 SQL로 쓰되, 타입 안전성은 유지.

5.3 Prisma Postgres — 매니지드 서비스

Prisma 사가 직접 운영하는 Postgres. 핵심 차별화는 3개.

  • 분리된 컴퓨트·스토리지 — Neon과 비슷한 아키텍처지만 unikernel 기반.
  • 콜드 스타트가 빠르다 — 서버리스 워크로드 친화적.
  • Prisma 도구체인과 직결prisma init 한 번에 DB 생성·스키마 푸시·클라이언트 생성까지.

매니지드 Postgres 시장은 이미 Neon·Supabase·PlanetScale Postgres·Vercel Postgres가 있어서 경쟁이 치열하지만, Prisma 사용자에게는 가장 친화적인 선택지가 됐다.

Prisma의 강점 (2026)

  • 압도적으로 친절한 학습 곡선·문서·튜토리얼.
  • 관계·페치·트랜잭션 API가 가장 매끄러움.
  • TypedSQL로 escape hatch가 정상화됨.
  • 엣지·번들 약점이 크게 줄었음.

Prisma의 약점 (2026)

  • 여전히 추상화 층이 가장 두꺼움 — 디버그할 때 SQL이 한 번 떨어졌다 도는 모델이 머릿속에 있어야 함.
  • 마이그레이션 도구(prisma migrate)는 강하지만, migrate dev의 자동 셰도우 DB 생성이 일부 호스팅 환경(권한 부족)에서 거슬리는 경우가 있음.
  • 라이선스·운영 비용 — Prisma Accelerate·Pulse 같은 매니지드 서비스가 있는데, 무료 한도 너머는 유료. 셀프 호스팅으로 가면 그만큼 일이 늘어남.

6장 · postgres.js·pg·mysql2 — ORM 없이 가기

가장 얇은 길. tagged template과 TypeScript의 추론·satisfies만으로 충분하다는 진영.

// db/index.ts
import postgres from 'postgres'

export const sql = postgres(process.env.DATABASE_URL!, {
  max: 10,
  idle_timeout: 20,
})

// 쿼리 — tagged template
type User = { id: number; email: string; name: string }
const users = await sql<User[]>`
  SELECT id, email, name
  FROM users
  WHERE email = ${'foo@example.com'}
  LIMIT 10
`
//   ^ users: User[]

postgres.js는 자체적으로 SQL injection 방지(파라미터 바인딩)와 prepared statement 캐싱을 한다. 타입은 손으로 선언하거나, kysely-codegen 같은 도구로 생성하거나, Zod로 런타임 검증한다.

Zod로 런타임 검증

import { z } from 'zod'

const UserRow = z.object({
  id: z.number(),
  email: z.string().email(),
  name: z.string(),
})

const rows = await sql<unknown[]>`SELECT id, email, name FROM users LIMIT 10`
const users = rows.map((r) => UserRow.parse(r))
//   ^ users: z.infer<typeof UserRow>[]

이 패턴의 장점은 DB가 실제로 무엇을 돌려주는지를 한 번 검증한다는 것. ORM·쿼리 빌더의 타입은 컴파일 타임 약속일 뿐, 실제 DB가 약속을 어겼는지(컬럼 NULL·잘못된 타입) 잡지는 못한다.

Raw 클라이언트의 강점

  • 가장 작고 가장 빠름 — 추가 추상화 0.
  • SQL 거의 전부 사용 가능 — 모든 Postgres 기능 그대로.
  • 엣지 호환성 최고 — postgres.js·pg 모두 Cloudflare Workers·Bun에서 잘 돈다. Neon HTTP 어댑터 등은 더 깔끔.
  • 러닝 커브가 SQL뿐.

Raw 클라이언트의 약점

  • 타입 안전은 너의 일.
  • 관계 쿼리는 직접 짜야 함.
  • 마이그레이션 도구도 직접 골라야 함.

7장 · 같은 쿼리, 네 가지 도구 — "email로 사용자 조회"

가장 단순한 쿼리부터. email로 user 한 명 찾고, 그 user의 최신 post 5개를 같이 가져오기.

7.1 Drizzle

import { db } from './db'
import { users, posts } from './db/schema'
import { eq, desc } from 'drizzle-orm'

async function findUserWithPosts(email: string) {
  const user = await db.query.users.findFirst({
    where: eq(users.email, email),
    with: {
      posts: {
        orderBy: [desc(posts.id)],
        limit: 5,
      },
    },
  })
  return user
  // user: { id, email, name, createdAt, posts: Array of post rows } or undefined
}

db.query.users.findFirst는 Drizzle Relations API. 내부적으로 한 번의 SQL로 json_agg나 LATERAL join을 만들어 보낸다.

7.2 Kysely

import { db } from './db'
import { jsonArrayFrom } from 'kysely/helpers/postgres'

async function findUserWithPosts(email: string) {
  const user = await db
    .selectFrom('user')
    .where('email', '=', email)
    .select((eb) => [
      'id',
      'email',
      'name',
      jsonArrayFrom(
        eb
          .selectFrom('post')
          .whereRef('post.author_id', '=', 'user.id')
          .orderBy('post.id', 'desc')
          .limit(5)
          .select(['post.id', 'post.title', 'post.body'])
      ).as('posts'),
    ])
    .executeTakeFirst()
  return user
}

Kysely는 sub-select 헬퍼로 nested JSON을 만든다. jsonArrayFrom은 Postgres json_agg 한 줄 컴파일. SQL이 무엇이 될지 머릿속에 그대로 그려진다.

7.3 Prisma

import { prisma } from './db'

async function findUserWithPosts(email: string) {
  const user = await prisma.user.findUnique({
    where: { email },
    include: {
      posts: {
        orderBy: { id: 'desc' },
        take: 5,
      },
    },
  })
  return user
}

읽기 가장 쉽다. Prisma가 내부적으로 알아서 효율적인 쿼리를 만든다(예전엔 N+1 우려가 있었지만 새 엔진에서는 join strategy를 명시적으로 선택할 수 있다).

7.4 postgres.js (raw)

import { sql } from './db'

type UserRow = {
  id: number
  email: string
  name: string
  posts: Array<{ id: number; title: string; body: string }>
}

async function findUserWithPosts(email: string) {
  const rows = await sql<UserRow[]>`
    SELECT
      u.id, u.email, u.name,
      COALESCE(
        json_agg(json_build_object('id', p.id, 'title', p.title, 'body', p.body))
          FILTER (WHERE p.id IS NOT NULL),
        '[]'::json
      ) AS posts
    FROM users u
    LEFT JOIN LATERAL (
      SELECT id, title, body
      FROM posts
      WHERE author_id = u.id
      ORDER BY id DESC
      LIMIT 5
    ) p ON TRUE
    WHERE u.email = ${email}
    GROUP BY u.id
  `
  return rows[0]
}

길지만 정확히 무엇이 도는지 안다. COALESCE(json_agg(...) FILTER (WHERE ...)) 같은 패턴은 raw에서는 매번 손으로 짜야 한다(헬퍼 만들면 줄어듦).

비교 — 같은 쿼리, 다른 추상화

도구줄 수읽기 난이도SQL 가시성타입 추론
Drizzle짧음쉬움자동
Kysely높음자동
Prisma가장 짧음가장 쉬움낮음자동
postgres.js가장 김SQL 의존가장 높음수동/Zod

8장 · 6축 비교 — 골라잡기 좋게

8.1 추상화 정도

도구추상화 정도
postgres.js거의 없음
KyselySQL 빌더
Drizzle스키마 + SQL 빌더
Prisma풀 ORM
MikroORM·TypeORMActive Record·Data Mapper

8.2 마이그레이션 도구

도구빌트인 마이그레이션자동 생성비고
Prismaprisma migrateO셰도우 DB 필요
Drizzledrizzle-kitO보수적 자동 생성
KyselyKysely 마이그레이션 + 외부 도구XAtlas·dbmate 권장
postgres.js없음XAtlas·Sqitch·Flyway 등

8.3 엣지 런타임 호환성 (Cloudflare Workers·Vercel Edge·Bun)

도구엣지 호환비고
Drizzle매우 좋음모든 어댑터가 엣지 우선
Kysely좋음드라이버 의존, 보통 잘 됨
postgres.js좋음Neon HTTP·Hyperdrive 결합 시 매우 좋음
Prisma좋음새 엔진 이후 안정
MikroORM·TypeORM보통~제한데코레이터·reflect-metadata 의존

8.4 번들 크기 (압축, 클라이언트 코어만)

대략적인 감(릴리즈에 따라 다름).

도구압축 후 크기(대략)
postgres.js매우 작음
Kysely작음
Drizzle작음
Prisma중간 (새 엔진 이후 큰 폭 축소)
MikroORM·TypeORM

8.5 SQL escape hatch

도구escape hatch매끄러움
postgres.js본인이 SQL최고
Kyselysql 템플릿매우 좋음
Drizzlesql 템플릿매우 좋음
PrismaTypedSQL·$queryRaw좋음(TypedSQL 이후 정상)
MikroORM·TypeORMraw query보통

8.6 멀티 DB 지원

도구PostgresMySQLSQLite기타
DrizzleOOOD1·LibSQL·Bun·Neon·Planetscale 외
KyselyOOO어댑터 다수
PrismaOOOSQL Server·MongoDB(제한)
postgres.jsOXXPostgres 전용

9장 · 실제 마이그레이션 후기 — Prisma → Drizzle / Kysely

9.1 Prisma → Drizzle (가장 흔한 경로)

이 경로를 택하는 팀이 늘었다. 이유는 두 가지.

  1. 엣지/번들 — 콜드 스타트와 응답 시간에서 차이가 보인다. 새 Prisma 엔진으로 격차는 줄었지만 0은 아니다.
  2. SQL escape hatch — sql 템플릿이 Prisma의 TypedSQL보다 자연스럽다. TypedSQL은 별도 .sql 파일이 필요하고, 동적 SQL에는 제약이 있다.

전형적인 마이그레이션 단계.

  • 1단계: 새 코드만 Drizzle로. 기존 Prisma 코드는 그대로.
  • 2단계: drizzle-kit으로 기존 DB introspect → 스키마 코드 생성.
  • 3단계: read 경로부터 점진 치환. write·트랜잭션은 나중에.
  • 4단계: 마이그레이션 도구 교체(prisma migratedrizzle-kit).

잘 안 되는 부분

  • Prisma migrations의 히스토리는 Drizzle Kit이 모른다 — 둘이 한동안 공존하는 동안 충돌이 안 나게 관리해야 함.
  • enum·복합 PK·partial index 같은 가장자리 — Drizzle introspection이 100%를 잡지 못하는 경우 있음. 수동 보정 필요.
  • 관계 쿼리 의미 미세 차이 — Prisma include와 Drizzle with이 같아 보여도 LATERAL join 전략이 다르면 결과 JSON 모양·정렬이 미세하게 다를 수 있음.

9.2 Prisma → Kysely (SQL 팀의 경로)

DB 스키마 운영을 이미 Atlas·Sqitch 같은 도구로 하고 있는 팀, 또는 SQL을 그대로 짜고 싶은데 타입은 받고 싶은 팀이 자주 선택.

  • 1단계: kysely-codegen으로 live DB에서 타입 생성.
  • 2단계: 가장 복잡한 쿼리(window·CTE·json_agg)부터 치환 — 가장 큰 가치를 가장 먼저 본다.
  • 3단계: 마이그레이션은 Atlas·dbmate 등으로 분리(이미 그 단계인 팀이 많음).
  • 4단계: read 단순 쿼리도 차차 옮김. write·트랜잭션은 마지막.

잘 안 되는 부분

  • 관계 쿼리는 길어진다 — Prisma 한 줄이 Kysely 5~15줄이 되기도. 헬퍼 함수 만드는 일이 필요.
  • Selectable<DB["user"]> 같은 유틸리티 타입 이해 비용 — 처음 보는 사람에게 좀 낯섦.
  • 에코시스템이 작다 — 플러그인·예제·서드파티 통합이 Prisma만큼 풍성하진 않음.

10장 · MikroORM·TypeORM — 여전히 살아 있지만

MikroORM

엔티티 클래스 기반 Data Mapper ORM. identity map·unit of work·풍성한 관계 API가 강점. DDD 스타일 프로젝트나 도메인 객체 중심으로 짜는 팀이 좋아한다. SQL과 거리는 가장 멀다.

  • 강점: 도메인 객체 중심, Active Record/Data Mapper 모두 지원, 트랜잭션·관계가 매끄럽다.
  • 약점: 무겁다, 데코레이터·reflect-metadata 의존, 엣지 친화도 보통.

TypeORM

가장 오래된 TS ORM 중 하나. 데코레이터 기반 Active Record + Data Mapper. 유지보수가 일정치 않고 알려진 버그가 오래 남는 경향이 있어 신규 프로젝트의 첫 선택지는 아니다. 그러나 이미 TypeORM으로 짠 코드가 많으면 빨리 떼지 못한다.

  • 강점: 친숙함, 풍부한 데코레이터 API.
  • 약점: 유지보수 risk, TS 5.x 변화·데코레이터 표준 변경에 둔감, 엣지 호환 어려움.

신규 프로젝트에서 TypeORM을 자발적으로 고르는 경우는 2026년에 드물다. 이미 있는 코드베이스는 마이그레이션 비용·이득을 따로 따져야 한다.


11장 · 무엇을 언제 골라야 하나 — 솔직한 결론

자, 정답은? 항상 그렇듯 "상황 따라 다르다". 다만 패턴이 있다.

Drizzle을 고를 때

  • 신규 프로젝트, 엣지 런타임(Cloudflare Workers·Vercel Edge) 우선.
  • SQL을 알고/배우려는 팀.
  • 멀티 DB(SQLite·Postgres·MySQL·D1·LibSQL) 가능성.
  • 번들 크기·콜드 스타트가 중요.

Kysely를 고를 때

  • 마이그레이션은 이미 Atlas·Sqitch·dbmate 등으로 운영.
  • 복잡한 SQL(CTE·window·json_agg)이 많고, SQL을 직접 짜고 싶음.
  • 스키마 관리·DDL을 ORM과 분리하고 싶음.
  • 작은 의존성·작은 번들을 원함.

Prisma를 고를 때

  • 신규 풀스택 팀, 학습 곡선 최소화 우선.
  • 관계가 복잡하고 include/페치 매끄러움이 중요.
  • Prisma Postgres·Accelerate·Pulse 같은 매니지드 도구체인을 활용하고 싶음.
  • TypedSQL로 복잡 SQL을 따로 다뤄도 괜찮음.

postgres.js·pg(raw)를 고를 때

  • 팀 전체가 SQL에 강함.
  • 추가 추상화를 의식적으로 거부.
  • 가장 작은 번들·가장 빠른 런타임이 핵심.
  • 런타임 검증(Zod)이나 codegen으로 타입을 별도 관리 가능.

안티 체크리스트 (피해야 할 패턴)

  • 한 프로젝트에 ORM 두 개 — 학습·디버그 비용 2배.
  • "Prisma 느려서 Drizzle로 갈아탔다"는 결정을 측정 없이 내림 — 새 엔진 이후엔 적용 안 되는 경우 많음.
  • ORM 골라놓고 거의 모든 쿼리를 $queryRaw로 짬 — 그러면 ORM이 아니라 무거운 raw 클라이언트.
  • 스키마는 Drizzle, 마이그레이션은 Prisma 같은 어색한 조합 — 진실의 원천이 두 개.
  • 엣지 우선 환경인데 데코레이터 ORM 채택 — 빌드·번들·런타임 모두 가시밭.

에필로그 — 추상화는 도구이지 종교가 아니다

ORM·쿼리 빌더 선택은 추상화 정도와 escape hatch 매끄러움의 트레이드오프다. 한쪽이 우월하지 않다. 다만 2026년 5월의 풍경은 분명하다.

  • SQL에 더 가까운 쪽이 늘었다 — Drizzle·Kysely·raw 진영이 자랐다.
  • 풀 ORM은 다이어트 중 — Prisma 새 엔진과 TypedSQL은 약점 두 개를 정통으로 친다.
  • 엣지는 기본값이 됐다 — 어떤 도구를 골라도 엣지 호환을 무시할 수 없다.
  • '한 도구가 모든 걸 해결'은 점점 깨졌다 — DB 클라이언트 + 마이그레이션 도구 + 타입 생성 도구를 따로 조합하는 게 흔해졌다.

결정 체크리스트

  • 엣지 우선인가? — Yes면 Drizzle·Kysely·postgres.js·새 Prisma.
  • 팀이 SQL에 강한가? — Yes면 Kysely·postgres.js, No면 Prisma·Drizzle.
  • 관계 페치가 핵심인가? — Yes면 Prisma > Drizzle Relations > Kysely.
  • 복잡 SQL(CTE·window)이 많은가? — Yes면 Kysely·postgres.js > Drizzle > Prisma TypedSQL.
  • 마이그레이션 도구는? — Prisma·Drizzle Kit 빌트인 vs Atlas·dbmate 외부.
  • 멀티 DB 가능성은? — Yes면 Drizzle·Kysely.
  • 학습 곡선 최소화? — Prisma.

안티 패턴

  1. 측정 없이 ORM 갈아타기 — 결정은 측정 후에.
  2. 한 코드베이스에 ORM 둘 공존 — 종착지 정하고 마이그레이션 일정 잡기.
  3. ORM 쓰면서 $queryRaw 90% — ORM이 아니라 무거운 raw.
  4. 스키마 진실의 원천 두 개 — DB와 코드의 진실은 하나여야.
  5. 마이그레이션 자동화 없이 codegen — 타입이 거짓말한다.
  6. 엣지 우선인데 무거운 데코레이터 ORM — 빌드·런타임 둘 다 비용.
  7. 관계가 복잡한데 raw — 직접 짜야 할 SQL이 산더미.

다음 글 예고

다음 글 후보: Drizzle Relations v2 깊게 — json_agg·LATERAL join이 어떻게 만들어지는가, Prisma TypedSQL 한 달 써본 후기 — 어디에 쓰고 어디에 안 쓰나, Cloudflare Workers + Hyperdrive + postgres.js 풀스택 사례.

"SQL을 알면 모든 도구가 친절해진다. 모르면 어떤 도구도 마법이 아니다."

— TypeScript ORM·쿼리 빌더 2026, 끝.


참고 / References

TypeScript ORMs and Query Builders 2026 — Drizzle vs Kysely vs Prisma vs postgres.js Deep Dive (How Close to SQL?) (english)

Prologue — The "Prisma or raw SQL" Era Is Over

Even in 2026, a question shows up in every new TypeScript project's kickoff meeting.

"What are we using for the database?"

In 2022, the answer was usually one of two. Prisma (most popular, full ORM) or raw pg/mysql2 (write your own SQL, types are your problem). In between sat TypeORM and Sequelize, but TypeScript ergonomics and migration UX kept pushing them out of greenfield projects.

2026 looks different. Two newer contenders have settled in.

  • Drizzle ORM — the "closest-to-SQL" headless ORM. You declare your schema as TS code, and queries map 1:1 to SQL lines. Tiny bundle, edge-runtime first. As of May 2026, GitHub stars are over 31k and weekly npm downloads exceed 2M.
  • Kysely — a "schemaless pure query builder." It takes any DB client (pg, mysql2, better-sqlite3) and wraps it in a type-safe builder. Migrations and schema management are your job. GitHub stars over 12k.

And Prisma has its own two new cards.

  • Prisma engine rewrite — since Prisma 6, the old Rust-based query engine is being replaced in stages by a TypeScript-native client plus a Go-based lightweight engine. Cold starts dropped, edge compatibility is normal again, and bundle size went down meaningfully.
  • Prisma Postgres — a managed Postgres service operated by Prisma itself. Unikernel-on-bare-metal architecture with separated compute and storage. Generous free tier, fast cold starts.

And a wave shaking everyone.

  • postgres.js, pg, mysql2 directly — the camp arguing that tagged template literals plus TypeScript's satisfies and as const give you all the type safety you actually need, without a builder at all. "Query builders are over-abstraction."

This post compares these tools as of May 2026 along six axes: abstraction depth, migrations, edge fit, bundle, escape hatch, multi-DB. And we write the same query four ways.


1. Landscape — Tool Map

First, let's classify. Not everything is in the same lane.

CategoryToolOne-line summary
Full ORM (Active Record / Data Mapper)Prisma, MikroORM, TypeORM, SequelizeModels, relations, fetch, cache
Headless ORMDrizzleSchema-as-code, queries map 1:1 to SQL
Query builderKysely, KnexType-safe SQL composition, no schema
Raw client + type helperspostgres.js, pg, mysql2 + ZodTagged template, runtime validation
Relational DSLEdgeQL (EdgeDB), SurrealQLDB ships its own query language

This post focuses on the bolded four — Prisma, Drizzle, Kysely, postgres.js. MikroORM and TypeORM appear briefly at the end.

Why these four

  • Prisma — most famous, near-default for new full-stack projects. With the new engine and Postgres service, it is firmly in tier one again.
  • Drizzle — the fastest-growing tool of 2024-2026. Vercel's official guides, the Hono and Cloudflare Workers recommended stack, and one of the T3 stack defaults.
  • Kysely — the first tool teams meet when leaving Prisma. It shows up constantly in migration stories.
  • postgres.js — Porsager's fast, small Postgres client. Great fit with Bun and Cloudflare Workers. The de facto standard in the no-ORM camp.

2. The Abstraction Spectrum — How Much to Hide

A data-access library is a choice about how thick a layer to place between your code and the DB. The left is closer to SQL, the right is closer to objects.

SQL                                                              Objects
 |                                                                  |
postgres.js   --   Kysely   --   Drizzle   --   Prisma   --   MikroORM/TypeORM
(raw)             (builder)      (headless ORM)  (full ORM)     (Active Record)

Is more abstraction better? Definitely not. It's a trade-off.

  • Thinner side, pros: full SQL expressiveness, direct performance tuning, learning and debugging both converge to one thing — SQL.
  • Thicker side, pros: writing speed, direct mapping to domain objects, automatic relation and N+1 handling, richer IDE autocomplete.
  • Thinner side, cons: relation fetch is your job, domain mapping is separate.
  • Thicker side, cons: when the abstraction lacks something (window functions, CTEs, complex joins), the escape hatch is awkward, and you pay in bundle size, cold starts, and runtime compatibility.

The 2026 mood leans toward "close to SQL, but type-safe." Drizzle, Kysely, and postgres.js have all grown. That said, Prisma 6's engine rewrite makes the thick side much lighter, so "Prisma is heavy" is increasingly outdated.


3. Drizzle — "SQL Written in TypeScript"

Drizzle is one of the most-picked tools in greenfield 2026 projects. Three core ideas.

  1. Schema as TS code — declare tables, columns, and relations in schema.ts. The TS file is the source of truth, not the DB.
  2. Queries map 1:1 to SQLdb.select().from(users).where(eq(users.email, '...')). Anyone who knows SQL can immediately see what it compiles to.
  3. Headless and tiny — almost no runtime dependencies. Runs on Cloudflare Workers, Vercel Edge, and Deno Deploy as-is. Bundle is roughly 7-15 KB gzipped.

Drizzle schema

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

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

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  authorId: integer('author_id').notNull().references(() => users.id),
  title: text('title').notNull(),
  body: text('body').notNull(),
})

Migrations with Drizzle Kit

# after writing drizzle.config.ts
npx drizzle-kit generate   # generate SQL migration from schema changes
npx drizzle-kit migrate    # apply to the actual DB
npx drizzle-kit studio     # local GUI

Drizzle strengths

  • Natural SQL escape hatch — the sql tag lets you inject arbitrary SQL and type that fragment by hand.
  • Edge compatibility — every adapter (drizzle-orm/postgres-js, drizzle-orm/neon-http, drizzle-orm/d1) runs on edge runtimes.
  • Multi-DB — PostgreSQL, MySQL, SQLite, D1, LibSQL, Neon, Planetscale, Vercel Postgres, Bun SQLite. Switching cost is small.

Drizzle weaknesses

  • Relations API is still maturing — nested queries via relations are not yet as smooth as Prisma. The 2025 v2 release closed most of the gap.
  • Manual migration conflict resolution — teammates colliding on migration files have to hand-merge.
  • Learning curve — if you don't know SQL, it is less friendly than Prisma.

4. Kysely — "Schemaless Pure Query Builder"

Kysely takes a different path. It does not enforce a schema. Your DB schema is your problem (SQL files, Atlas, Sqitch, Liquibase, whatever), and Kysely lays a type-safe query builder on top.

// db/types.ts — TS types matching your DB schema
import type { ColumnType, Generated } from 'kysely'

export interface Database {
  user: UserTable
  post: PostTable
}

export interface UserTable {
  id: Generated<number>
  email: string
  name: string
  created_at: ColumnType<Date, string | undefined, never>
}

export interface PostTable {
  id: Generated<number>
  author_id: number
  title: string
  body: string
}

The trick is that Kysely takes the Database interface as a generic and infers column names and types in every query.

kysely-codegen for type generation

If you do not want to hand-write the schema interface, use kysely-codegen. It introspects a live DB and produces TS types like above.

npx kysely-codegen --url postgres://user:pw@localhost/db --out-file db/types.ts

Kysely strengths

  • Full SQL expressiveness — window functions, CTEs, json_agg, UNION ALL, almost everything maps 1:1.
  • DB and migration separation — pick your migration tool independently. Atlas, Sqitch, dbmate, Flyway.
  • Small bundle — core builder is around 14 KB gzipped.
  • Minimal runtime deps — fine on Cloudflare Workers, Bun, Deno.

Kysely weaknesses

  • Keeping schema and types honest is on you — if you forget to re-run kysely-codegen after a migration, your types lie. Wire it into CI or a post-migration hook.
  • Explicit relation fetches — N+1 patterns, JOIN, json_agg are all yours to write. Not as short as Prisma or Drizzle.
  • Smaller ecosystem — fewer plugins, tutorials, and adapters than the other two.

5. Prisma — New Engine and Prisma Postgres

Prisma went through two big shifts in 2024-2025.

5.1 Engine rewrite — from Rust to TS-native plus Go

In Prisma 5, the query engine was a separate Rust process (or WASM). Cold starts were slow and edge runtimes were awkward. From Prisma 6 onward, the staged rewrite changed this.

  • TypeScript-native client — much of the query compiler moved to TS. The client builds SQL in-process.
  • Go-based lightweight engine — used on the server side for managed services like Prisma Accelerate and Pulse.
  • Smaller bundle — client size dropped meaningfully. Exact numbers depend on setup; see Prisma's own benchmarks and release notes.
  • Edge compatibility normalized — the adapters running on Vercel Edge and Cloudflare Workers stabilized.

5.2 TypedSQL — typed raw SQL inside Prisma

Prisma's classic weakness was "complex SQL is awkward." TypedSQL attacks that head on.

-- prisma/sql/findActiveUsers.sql
SELECT id, email, name
FROM "User"
WHERE last_seen_at > $1
ORDER BY last_seen_at DESC
LIMIT $2;
import { PrismaClient } from '@prisma/client'
import { findActiveUsers } from '@prisma/client/sql'

const prisma = new PrismaClient()
const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
const users = await prisma.$queryRawTyped(findActiveUsers(since, 50))
//    users: Array of typed rows inferred from the SQL parameters

The .sql file is checked at build time, with parameter and result types generated automatically. SQL stays SQL, but stays type-safe.

5.3 Prisma Postgres — managed service

A managed Postgres operated by Prisma. Three differentiators.

  • Separated compute and storage — Neon-like, but unikernel-based.
  • Fast cold starts — friendly to serverless workloads.
  • Tight integration with Prisma toolingprisma init to DB creation, schema push, and client generation in one go.

The managed Postgres market is crowded (Neon, Supabase, PlanetScale Postgres, Vercel Postgres), but for Prisma users it became the most cohesive choice.

Prisma strengths (2026)

  • The friendliest learning curve, docs, and tutorials by a wide margin.
  • The smoothest relation, fetch, and transaction APIs.
  • TypedSQL makes the escape hatch first-class.
  • Edge and bundle weaknesses have shrunk dramatically.

Prisma weaknesses (2026)

  • Still the thickest abstraction — when debugging, you have to mentally translate to SQL anyway.
  • prisma migrate dev's shadow-DB requirement is annoying in hosting environments with permission limits.
  • License and operating cost — Accelerate and Pulse are managed services with free tiers, but past those you pay or self-host (which adds work).

6. postgres.js, pg, mysql2 — Going Without an ORM

The thinnest path. Tagged templates and TS inference plus satisfies are arguably enough.

// db/index.ts
import postgres from 'postgres'

export const sql = postgres(process.env.DATABASE_URL!, {
  max: 10,
  idle_timeout: 20,
})

// query — tagged template
type User = { id: number; email: string; name: string }
const users = await sql<User[]>`
  SELECT id, email, name
  FROM users
  WHERE email = ${'foo@example.com'}
  LIMIT 10
`
//   users: User[]

postgres.js handles SQL injection prevention via parameter binding and caches prepared statements. You declare types by hand, generate them with kysely-codegen or similar, or validate at runtime with Zod.

Zod for runtime validation

import { z } from 'zod'

const UserRow = z.object({
  id: z.number(),
  email: z.string().email(),
  name: z.string(),
})

const rows = await sql<unknown[]>`SELECT id, email, name FROM users LIMIT 10`
const users = rows.map((r) => UserRow.parse(r))
//   users: z.infer<typeof UserRow>[]

The win here is that you verify once what the DB actually returned. ORM and builder types are compile-time promises, not proof the DB kept its end (NULL columns, type drift).

Raw client strengths

  • Smallest, fastest — zero extra abstraction.
  • Full SQL — every Postgres feature is available unchanged.
  • Best edge fit — postgres.js and pg run well on Cloudflare Workers and Bun. Neon HTTP adapter is even cleaner.
  • Only SQL to learn.

Raw client weaknesses

  • Type safety is your job.
  • Relation queries are hand-written.
  • Pick your own migration tool.

7. Same Query, Four Tools — "Find user by email with latest posts"

The simplest example. Find one user by email and return their 5 most recent posts inline.

7.1 Drizzle

import { db } from './db'
import { users, posts } from './db/schema'
import { eq, desc } from 'drizzle-orm'

async function findUserWithPosts(email: string) {
  const user = await db.query.users.findFirst({
    where: eq(users.email, email),
    with: {
      posts: {
        orderBy: [desc(posts.id)],
        limit: 5,
      },
    },
  })
  return user
  // user: User row with posts: Array of post rows, or undefined
}

db.query.users.findFirst is the Drizzle Relations API. Internally it issues one SQL statement with json_agg or a LATERAL join.

7.2 Kysely

import { db } from './db'
import { jsonArrayFrom } from 'kysely/helpers/postgres'

async function findUserWithPosts(email: string) {
  const user = await db
    .selectFrom('user')
    .where('email', '=', email)
    .select((eb) => [
      'id',
      'email',
      'name',
      jsonArrayFrom(
        eb
          .selectFrom('post')
          .whereRef('post.author_id', '=', 'user.id')
          .orderBy('post.id', 'desc')
          .limit(5)
          .select(['post.id', 'post.title', 'post.body'])
      ).as('posts'),
    ])
    .executeTakeFirst()
  return user
}

Kysely composes nested JSON via sub-select helpers. jsonArrayFrom compiles to Postgres json_agg directly. You can predict the SQL exactly.

7.3 Prisma

import { prisma } from './db'

async function findUserWithPosts(email: string) {
  const user = await prisma.user.findUnique({
    where: { email },
    include: {
      posts: {
        orderBy: { id: 'desc' },
        take: 5,
      },
    },
  })
  return user
}

Easiest to read. Prisma picks the efficient query internally (the new engine lets you choose join strategy explicitly to avoid the old N+1 worry).

7.4 postgres.js (raw)

import { sql } from './db'

type UserRow = {
  id: number
  email: string
  name: string
  posts: Array<{ id: number; title: string; body: string }>
}

async function findUserWithPosts(email: string) {
  const rows = await sql<UserRow[]>`
    SELECT
      u.id, u.email, u.name,
      COALESCE(
        json_agg(json_build_object('id', p.id, 'title', p.title, 'body', p.body))
          FILTER (WHERE p.id IS NOT NULL),
        '[]'::json
      ) AS posts
    FROM users u
    LEFT JOIN LATERAL (
      SELECT id, title, body
      FROM posts
      WHERE author_id = u.id
      ORDER BY id DESC
      LIMIT 5
    ) p ON TRUE
    WHERE u.email = ${email}
    GROUP BY u.id
  `
  return rows[0]
}

Long, but you know exactly what runs. The COALESCE(json_agg(...) FILTER (WHERE ...)) idiom has to be hand-written every time in raw (you can factor it into helpers).

Comparison — Same query, different abstractions

ToolLinesRead difficultySQL visibilityType inference
DrizzleShortEasyMediumAutomatic
KyselyMediumMediumHighAutomatic
PrismaShortestEasiestLowAutomatic
postgres.jsLongestSQL-dependentHighestManual or Zod

8. Six-Axis Comparison — For Quick Picking

8.1 Abstraction depth

ToolDepth
postgres.jsAlmost none
KyselySQL builder
DrizzleSchema plus SQL builder
PrismaFull ORM
MikroORM, TypeORMActive Record, Data Mapper

8.2 Migration tooling

ToolBuilt-in migrationsAuto generationNotes
Prismaprisma migrateYesNeeds shadow DB
Drizzledrizzle-kitYesConservative auto-generation
KyselyKysely migrations plus external toolsNoAtlas, dbmate recommended
postgres.jsNoneNoAtlas, Sqitch, Flyway, etc.

8.3 Edge-runtime fit (Cloudflare Workers, Vercel Edge, Bun)

ToolEdge fitNotes
DrizzleExcellentAll adapters edge-first
KyselyGoodDriver-dependent, usually fine
postgres.jsGoodExcellent with Neon HTTP or Hyperdrive
PrismaGoodStable after the new engine
MikroORM, TypeORMModerate to limitedDecorator and reflect-metadata dependency

8.4 Bundle size (gzipped, client core only)

Rough feel; release-dependent.

ToolApprox gzipped size
postgres.jsVery small
KyselySmall
DrizzleSmall
PrismaMedium (substantially smaller after the new engine)
MikroORM, TypeORMLarge

8.5 SQL escape hatch

ToolEscape hatchSmoothness
postgres.jsYou are writing SQLBest
Kyselysql tagVery smooth
Drizzlesql tagVery smooth
PrismaTypedSQL plus $queryRawGood, normalized since TypedSQL
MikroORM, TypeORMRaw queryModerate

8.6 Multi-DB support

ToolPostgresMySQLSQLiteOthers
DrizzleYesYesYesD1, LibSQL, Bun, Neon, Planetscale, more
KyselyYesYesYesMany adapters
PrismaYesYesYesSQL Server, MongoDB (limited)
postgres.jsYesNoNoPostgres only

9. Real Migration Stories — Prisma to Drizzle, Prisma to Kysely

9.1 Prisma to Drizzle (most common path)

This is the path more teams have been taking. Two reasons.

  1. Edge and bundle — cold start and response time differences are visible. The new Prisma engine narrowed the gap but did not eliminate it.
  2. SQL escape hatch — Drizzle's sql tag feels more natural than Prisma's TypedSQL. TypedSQL requires separate .sql files and constrains dynamic SQL.

Typical migration steps.

  • Step 1: new code in Drizzle. Leave existing Prisma code as-is.
  • Step 2: drizzle-kit introspects the existing DB and generates schema code.
  • Step 3: replace read paths first. Writes and transactions later.
  • Step 4: swap the migration tool (prisma migrate to drizzle-kit).

Rough edges

  • Prisma migration history is invisible to Drizzle Kit — while both coexist, you have to manage non-collision yourself.
  • Edge cases like enums, composite PKs, partial indexes — Drizzle introspection misses some. Hand-fix as needed.
  • Subtle relation semantics — Prisma include and Drizzle with look identical but may differ in LATERAL join strategy, producing slightly different JSON shape or ordering.

9.2 Prisma to Kysely (SQL-first teams)

A common pick for teams already running DB schema via Atlas or Sqitch, or for teams who want to write SQL but still get types.

  • Step 1: generate types with kysely-codegen from a live DB.
  • Step 2: replace the most complex queries first (window, CTE, json_agg) — that is where you get the biggest payoff.
  • Step 3: separate migrations into Atlas, dbmate, etc. — many teams are there already.
  • Step 4: convert simpler read queries gradually. Writes and transactions last.

Rough edges

  • Relation queries get longer — a Prisma one-liner can become 5-15 Kysely lines. You will want helper functions.
  • Utility types like Selectable<DB["user"]> have a cost — unfamiliar to first-time readers.
  • Smaller ecosystem — fewer plugins, examples, third-party integrations than Prisma.

10. MikroORM and TypeORM — Still Alive, But

MikroORM

Entity-class-based Data Mapper. Strong on identity map, unit of work, and rich relation APIs. Favored by DDD-style projects and domain-object-first teams. The furthest from SQL.

  • Strengths: domain-object first, supports Active Record and Data Mapper, smooth transactions and relations.
  • Weaknesses: heavy, decorator and reflect-metadata dependency, modest edge fit.

TypeORM

One of the oldest TS ORMs. Decorator-based Active Record plus Data Mapper. Maintenance is uneven and known bugs linger, so it is no longer the first pick for greenfield. But if you have a TypeORM-shaped codebase, you cannot rip it out fast.

  • Strengths: familiarity, rich decorator API.
  • Weaknesses: maintenance risk, sluggish to keep up with TS 5.x and decorator standard changes, edge compatibility is hard.

Voluntarily choosing TypeORM for a new project is rare in 2026. Existing codebases need a separate cost-benefit analysis.


11. What to Pick, When — An Honest Take

The answer? As always, it depends. But there are patterns.

Pick Drizzle when

  • New project, edge runtimes (Cloudflare Workers, Vercel Edge) come first.
  • Team knows or wants to learn SQL.
  • Multi-DB possibility (SQLite, Postgres, MySQL, D1, LibSQL).
  • Bundle size and cold starts matter.

Pick Kysely when

  • Migrations already run on Atlas, Sqitch, dbmate.
  • Lots of complex SQL (CTE, window, json_agg), and you want to write SQL directly.
  • You want schema management and DDL decoupled from the ORM.
  • Tiny deps and small bundle matter.

Pick Prisma when

  • New full-stack team, minimum learning curve wins.
  • Relations are complex and smooth include and fetch are important.
  • You want Prisma Postgres, Accelerate, Pulse, or other managed tools.
  • You are fine handling complex SQL via TypedSQL separately.

Pick postgres.js or pg (raw) when

  • Whole team is strong with SQL.
  • You consciously reject extra abstraction.
  • Smallest bundle and fastest runtime are core requirements.
  • Types are managed separately via runtime validation (Zod) or codegen.

Anti-checklist (patterns to avoid)

  • Two ORMs in one project — double learning and debugging cost.
  • "Prisma was slow so we switched to Drizzle" decided without measuring — frequently no longer applies after the new engine.
  • ORM picked but almost every query is $queryRaw — that is not an ORM, that is a heavy raw client.
  • Schema in Drizzle, migrations in Prisma — two sources of truth, bad time.
  • Edge-first environment with a decorator ORM — build, bundle, and runtime are all thorny.

Epilogue — Abstraction Is a Tool, Not a Religion

Picking an ORM or query builder is a trade-off between abstraction depth and escape-hatch smoothness. Neither side is superior. But May 2026's landscape is clear.

  • The SQL-closer side has grown — Drizzle, Kysely, and raw clients have all risen.
  • Full ORMs are on a diet — Prisma's new engine and TypedSQL hit the two classic weaknesses head-on.
  • Edge became default — whichever tool you pick, you cannot ignore edge compatibility.
  • "One tool solves everything" is gone — composing DB client + migration tool + type generator is increasingly the norm.

Decision checklist

  • Edge first? — If yes, pick Drizzle, Kysely, postgres.js, or the new Prisma.
  • Is your team strong with SQL? — If yes, Kysely or postgres.js; if no, Prisma or Drizzle.
  • Relation fetch heavy? — Then Prisma is greater than Drizzle Relations is greater than Kysely.
  • Lots of complex SQL (CTE, window)? — Kysely or postgres.js is greater than Drizzle is greater than Prisma TypedSQL.
  • Migration tool? — Prisma or Drizzle Kit built-in, or external Atlas or dbmate.
  • Multi-DB possibility? — If yes, Drizzle or Kysely.
  • Minimize learning curve? — Prisma.

Anti-patterns

  1. Switching ORMs without measuring — decide after numbers.
  2. Two ORMs coexisting in one codebase — pick a destination and schedule the migration.
  3. ORM with 90% $queryRaw — that is a heavy raw client.
  4. Two schema sources of truth — DB and code must agree on one.
  5. Codegen without migration automation — types lie.
  6. Edge-first with heavy decorator ORM — build and runtime both pay.
  7. Complex relations with raw — your hand-written SQL pile grows fast.

Next post candidates

Possibilities: deep dive on Drizzle Relations v2 — how json_agg and LATERAL joins are emitted, one month with Prisma TypedSQL — what to use it for and what not, full-stack case study on Cloudflare Workers + Hyperdrive + postgres.js.

"Once you know SQL, every tool feels friendly. If you don't, no tool is magic."

— TypeScript ORMs and Query Builders 2026, end.


References