LabHub

Blog

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

한국어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.

And Prisma has its own two new cards.

And a wave shaking everyone.

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


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.

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

Drizzle weaknesses


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

Kysely weaknesses


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.

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.

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)

Prisma weaknesses (2026)


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

Raw client weaknesses


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.

Rough edges

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.

Rough edges


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.

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.

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

Pick Kysely when

Pick Prisma when

Pick postgres.js or pg (raw) when

Anti-checklist (patterns to avoid)


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.

Decision checklist

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

Comments

No comments yet.

Sign in to leave a comment