Skip to content

✍️ 필사 모드: TypeScript ORMs and Query Builders 2026 — Drizzle vs Kysely vs Prisma vs postgres.js Deep Dive (How Close to SQL?) (english)

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

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

현재 단락 (1/380)

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

작성 글자: 0원문 글자: 21,155작성 단락: 0/380