✍️ 필사 모드: TypeScript ORMs and Query Builders 2026 — Drizzle vs Kysely vs Prisma vs postgres.js Deep Dive (How Close to SQL?) (english)
EnglishPrologue — 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
satisfiesandas constgive 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.
| Category | Tool | One-line summary |
|---|---|---|
| Full ORM (Active Record / Data Mapper) | Prisma, MikroORM, TypeORM, Sequelize | Models, relations, fetch, cache |
| Headless ORM | Drizzle | Schema-as-code, queries map 1:1 to SQL |
| Query builder | Kysely, Knex | Type-safe SQL composition, no schema |
| Raw client + type helpers | postgres.js, pg, mysql2 + Zod | Tagged template, runtime validation |
| Relational DSL | EdgeQL (EdgeDB), SurrealQL | DB 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.
- Schema as TS code — declare tables, columns, and relations in
schema.ts. The TS file is the source of truth, not the DB. - Queries map 1:1 to SQL —
db.select().from(users).where(eq(users.email, '...')). Anyone who knows SQL can immediately see what it compiles to. - 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
sqltag 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-codegenafter a migration, your types lie. Wire it into CI or a post-migration hook. - Explicit relation fetches — N+1 patterns,
JOIN,json_aggare 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 tooling —
prisma initto 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
pgrun 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
| Tool | Lines | Read difficulty | SQL visibility | Type inference |
|---|---|---|---|---|
| Drizzle | Short | Easy | Medium | Automatic |
| Kysely | Medium | Medium | High | Automatic |
| Prisma | Shortest | Easiest | Low | Automatic |
| postgres.js | Longest | SQL-dependent | Highest | Manual or Zod |
8. Six-Axis Comparison — For Quick Picking
8.1 Abstraction depth
| Tool | Depth |
|---|---|
| postgres.js | Almost none |
| Kysely | SQL builder |
| Drizzle | Schema plus SQL builder |
| Prisma | Full ORM |
| MikroORM, TypeORM | Active Record, Data Mapper |
8.2 Migration tooling
| Tool | Built-in migrations | Auto generation | Notes |
|---|---|---|---|
| Prisma | prisma migrate | Yes | Needs shadow DB |
| Drizzle | drizzle-kit | Yes | Conservative auto-generation |
| Kysely | Kysely migrations plus external tools | No | Atlas, dbmate recommended |
| postgres.js | None | No | Atlas, Sqitch, Flyway, etc. |
8.3 Edge-runtime fit (Cloudflare Workers, Vercel Edge, Bun)
| Tool | Edge fit | Notes |
|---|---|---|
| Drizzle | Excellent | All adapters edge-first |
| Kysely | Good | Driver-dependent, usually fine |
| postgres.js | Good | Excellent with Neon HTTP or Hyperdrive |
| Prisma | Good | Stable after the new engine |
| MikroORM, TypeORM | Moderate to limited | Decorator and reflect-metadata dependency |
8.4 Bundle size (gzipped, client core only)
Rough feel; release-dependent.
| Tool | Approx gzipped size |
|---|---|
| postgres.js | Very small |
| Kysely | Small |
| Drizzle | Small |
| Prisma | Medium (substantially smaller after the new engine) |
| MikroORM, TypeORM | Large |
8.5 SQL escape hatch
| Tool | Escape hatch | Smoothness |
|---|---|---|
| postgres.js | You are writing SQL | Best |
| Kysely | sql tag | Very smooth |
| Drizzle | sql tag | Very smooth |
| Prisma | TypedSQL plus $queryRaw | Good, normalized since TypedSQL |
| MikroORM, TypeORM | Raw query | Moderate |
8.6 Multi-DB support
| Tool | Postgres | MySQL | SQLite | Others |
|---|---|---|---|---|
| Drizzle | Yes | Yes | Yes | D1, LibSQL, Bun, Neon, Planetscale, more |
| Kysely | Yes | Yes | Yes | Many adapters |
| Prisma | Yes | Yes | Yes | SQL Server, MongoDB (limited) |
| postgres.js | Yes | No | No | Postgres 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.
- Edge and bundle — cold start and response time differences are visible. The new Prisma engine narrowed the gap but did not eliminate it.
- SQL escape hatch — Drizzle's
sqltag feels more natural than Prisma's TypedSQL. TypedSQL requires separate.sqlfiles and constrains dynamic SQL.
Typical migration steps.
- Step 1: new code in Drizzle. Leave existing Prisma code as-is.
- Step 2:
drizzle-kitintrospects the existing DB and generates schema code. - Step 3: replace read paths first. Writes and transactions later.
- Step 4: swap the migration tool (
prisma migratetodrizzle-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
includeand Drizzlewithlook 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-codegenfrom 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-metadatadependency, 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
includeand 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
- Switching ORMs without measuring — decide after numbers.
- Two ORMs coexisting in one codebase — pick a destination and schedule the migration.
- ORM with 90%
$queryRaw— that is a heavy raw client. - Two schema sources of truth — DB and code must agree on one.
- Codegen without migration automation — types lie.
- Edge-first with heavy decorator ORM — build and runtime both pay.
- 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
- Drizzle ORM Official
- Drizzle ORM GitHub
- Drizzle Kit Documentation
- Drizzle Relations API
- Kysely Official
- Kysely GitHub
- kysely-codegen
- Prisma Official
- Prisma TypedSQL Documentation
- Prisma Postgres
- Prisma Engine Rewrite — Official Blog
- postgres.js GitHub — porsager/postgres
- node-postgres (pg) Official
- mysql2 GitHub
- MikroORM Official
- TypeORM Official
- Neon — Serverless Postgres
- Cloudflare Hyperdrive
- Cloudflare D1
- Atlas — Modern DB Migration
- Vercel Postgres
- Zod
- Hono — Edge Web Framework
- T3 Stack
현재 단락 (1/380)
Even in 2026, a question shows up in every new TypeScript project's kickoff meeting.