· drizzle / kysely / orm

Drizzle ORM vs Kysely: Which Typed Query Tool in 2026?

Drizzle wins on DX, auto-migrations, and first-party edge adapters. Kysely wins on compile-time query validation for complex schemas. Here is which one to pick.

By

1,741 words · 9 min read

Pick Drizzle if you are starting a greenfield TypeScript project and want schema-first development, automatic SQL migrations, and edge-ready adapters out of the box. Pick Kysely if your query patterns are genuinely complex — deeply nested subqueries, CTEs with aggregations, JSONB path operations — and you need the type system to catch invalid queries before they reach production.

Who this is for

TypeScript developers starting a new Postgres or SQLite project in 2026, or migrating away from Prisma. If you are already six months deep into one of these tools and your queries are working, keep going — this is not worth a migration just for the sake of it.

What we tested

We ran both tools on a three-table Postgres 16 schema: users, posts, and comments, with the full range of query patterns from CRUD to CTEs to JSONB aggregations. The host was Neon serverless Postgres — both tools have documented first-class support for it.

PackageStable versionRCReleased
drizzle-orm0.45.2v1.0.0-rc.3rc.3 released May 18, 2026
drizzle-kitbundled
kysely0.29.2May 16, 2026

Community traction as of May 19, 2026:

MetricDrizzleKysely
Weekly npm downloads9,695,5596,010,148
GitHub stars34,45913,825
Open issues1,784155

The open-issue gap is partly the larger user base, partly the more complex feature surface — it is worth knowing about, not worth panicking over.

Schema definition

Drizzle is schema-first. You define tables in TypeScript, run drizzle-kit generate, and it produces SQL migration files:

import { pgTable, serial, varchar, integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 256 }),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  authorId: integer('author_id').references(() => users.id),
  title: varchar('title', { length: 512 }),
});

Kysely flips this around. There is no schema file — you write TypeScript interfaces that describe your database, and you point Kysely at them:

interface Database {
  users: UsersTable;
  posts: PostsTable;
}

export const db = new Kysely<Database>({
  dialect: new NeonDialect({ connectionString: process.env.DATABASE_URL! }),
});

For greenfield, Drizzle’s approach is faster to productive. For brownfield — existing database, no interest in a TypeScript schema rewrite — Kysely lets you add type safety without touching the schema.

Query composition

Both use fluent builder APIs. Drizzle’s imports are table entities; Kysely’s are string references.

Drizzle:

const result = await db
  .select({ name: users.name, postCount: count(posts.id) })
  .from(users)
  .leftJoin(posts, eq(posts.authorId, users.id))
  .groupBy(users.id)
  .where(gt(users.id, 100));

Kysely:

const result = await db
  .selectFrom('users as u')
  .select(['u.name', db.fn.count<number>('posts.id').as('postCount')])
  .leftJoin('posts', 'posts.author_id', 'u.id')
  .groupBy('u.id')
  .where('u.id', '>', 100)
  .execute();

For standard CRUD and simple joins, both are good. The divergence shows up once queries get complicated.

Complex queries: CTEs, JSONB, nested results

This is where the tools split clearly.

CTEs

Drizzle supports CTEs but type inference can fall apart when you use the sql template tag inside a CTE without an explicit alias. The field type becomes DrizzleTypeError and the downstream reference is unreachable:

const sq = db.$with('post_counts').as(
  db.select({ userId: posts.authorId, cnt: sql<number>`count(*)` })
    .from(posts).groupBy(posts.authorId)
);
const result = await db.with(sq).select().from(sq);

Kysely propagates types correctly through CTEs without requiring manual annotation:

const result = await db
  .with('post_counts', (db) =>
    db.selectFrom('posts')
      .select(['author_id as userId', db.fn.countAll<number>().as('cnt')])
      .groupBy('author_id')
  )
  .selectFrom('post_counts')
  .selectAll()
  .execute();

Nested results and JSONB

Kysely ships jsonArrayFrom and jsonObjectFrom helpers that return correctly typed nested arrays in a single query:

import { jsonArrayFrom } from 'kysely/helpers/postgres';

const result = await db
  .selectFrom('person')
  .select((eb) => [
    'id',
    jsonArrayFrom(
      eb.selectFrom('pet')
        .select(['pet.id as pet_id', 'pet.name'])
        .whereRef('pet.owner_id', '=', 'person.id')
        .orderBy('pet.name')
    ).as('pets')
  ])
  .execute();
// Type: { id: string; pets: { pet_id: string; name: string }[] }[]

Drizzle’s SQL-like API returns flat types. To get nested results, you switch to the Relational API (RQB), which needs explicit with: declarations set up alongside your schema:

const result = await db.query.users.findMany({
  with: { posts: true },
});
// Type: { id: number; name: string; posts: { id: number; title: string }[] }[]

The RQB approach works well when your schema is fully mapped. For JSONB path operations (PostgreSQL ->>, #>>, @>), both tools make you drop to the sql template tag — Drizzle’s JSONB column types and Kysely’s helpers don’t fully cover the operator surface.

Query-level type safety

This is the core philosophical difference. Kysely validates the query structure at compile time. Its callback-based API tracks which tables and columns are in scope per subquery, and a reference to a column that isn’t in scope is a TypeScript error before you run anything.

Drizzle validates query results. The schema-derived types ensure SELECT results are correctly typed, but the builder is less strict about what’s expressible. Complex custom aggregations need explicit sql<Type> annotations with no runtime validation.

A team observed this (dev.to, 2025): Drizzle didn’t catch invalid column references in deeply nested queries, while Kysely’s callback scoping caught them at compile time. For CRUD-heavy apps, this distinction rarely matters. For analytics dashboards, data pipelines, or anything with 5+ join hops — it matters a lot.

Migration story

Drizzle is the clear winner here.

drizzle-kit is a full migration toolkit bundled with Drizzle:

# Detect schema changes, generate .sql migration file
npx drizzle-kit generate

# Apply pending migrations
npx drizzle-kit migrate

# Rapid prototyping: push schema directly, skip the file
npx drizzle-kit push

drizzle-kit pull reverses-engineers an existing database into a TypeScript schema — useful for teams migrating from Prisma or from raw SQL. Drizzle Studio (npx drizzle-kit studio) gives you a browser UI for exploring and editing data.

Kysely ships a Migrator class. Migrations are plain TypeScript files you write manually:

export async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable('users')
    .addColumn('id', 'serial', (col) => col.primaryKey())
    .addColumn('name', 'varchar(256)')
    .execute();
}

export async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable('users').execute();
}

There is no auto-generation. You write every migration by hand. Community tools like kysely-migration-cli or ley can help with the workflow, but it’s still manual by default. That is fine if you want full control and have the discipline. For most teams, Drizzle’s auto-generated migrations save real time.

Edge runtime

Drizzle is explicitly designed for the edge. First-party adapters for Cloudflare Workers (D1, Neon HTTP), Vercel Edge, Deno Deploy, Bun, Supabase Edge Functions, and more. The adapters are maintained by the Drizzle team and tracked against upstream breaking changes.

Kysely reaches the same targets through community dialects: kysely-d1 for Cloudflare D1, kysely-neon for Neon, kysely-libsql for Turso. The dialects are solid but community-maintained — there is occasionally a lag when the upstream driver changes.

For teams targeting Cloudflare Workers or Vercel Edge with Neon as the database, Drizzle’s setup is straightforward:

// src/db.ts
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql);

Kysely with Neon needs the community dialect:

import { Kysely } from 'kysely';
import { NeonDialect } from 'kysely-neon';

export const db = new Kysely<Database>({
  dialect: new NeonDialect({ connectionString: process.env.DATABASE_URL! }),
});

Both work. Drizzle is one fewer dependency.

Debugging

Drizzle Studio is a genuine advantage for debugging — it’s a local web UI for browsing and editing your data, launched with a single command. Drizzle v1.0.0-rc.1 (April 30, 2026) added opt-in JIT mappers with claimed 25–30% reduction in row-mapping latency (release notes).

Drizzle’s TypeScript error messages for complex query types have improved with v1.0 but can still produce confusing DrizzleTypeError values when you miss an alias.

Kysely’s errors are generally cleaner at compile time. For very complex queries, you can occasionally hit TypeScript’s “Type instantiation is excessively deep” limit — Kysely documents the workarounds in their recipes. Kysely’s 155 open issues versus Drizzle’s 1,784 reflects a smaller bug surface and simpler scope, not just a smaller user base.

Drizzle vs Kysely: Verdict

Pick Drizzle for new projects. Schema-first development, auto-generated migrations, Drizzle Studio, first-party edge adapters — it’s what most TypeScript teams should reach for in 2026. It’s faster to productive than Prisma with less abstraction overhead, and v0.45.2 is production-stable now. The v1.0 RC is shaping up well, but don’t wait for it if you’re starting today.

Pick Kysely when your query patterns go beyond standard CRUD. Deeply nested subqueries, CTEs with aggregations, dynamic column selection, or JSONB path operations — Kysely’s compile-time query validation catches errors that Drizzle lets through. Also the right call if you are migrating an existing Knex.js codebase (similar API shape) or if your database already exists and you don’t want to rewrite a schema in TypeScript DSL.

Both tools output raw SQL. Neither adds a query engine overhead. The choice is about dev loop and type-safety tradeoffs, not performance.

We tested both on Neon serverless Postgres — the setup for both is well-documented and works reliably at the edge. If you’re still deciding between more TypeScript data-access tools, Best TypeScript ORM in 2026 covers Prisma, Drizzle, and others side by side.

Caveats

  • Versions tested: drizzle-orm 0.45.2, drizzle-orm v1.0.0-rc.3, kysely 0.29.2, Postgres 16, Neon serverless host.
  • MySQL dialect not covered. Drizzle has a MySQL adapter; Kysely has MySQL community dialects. Both work, but MySQL-specific features (e.g. JSON_TABLE) have different support levels.
  • Drizzle’s Relations API (RQB) is a separate abstraction on top of the SQL-like API. If you use it exclusively, some of the CTE and JSONB caveats above don’t apply — but you give up SQL-level control.
  • Neon is an affiliate partner (see disclosure above). It didn’t change the verdict — Drizzle would win the migration section regardless of which Postgres host you use.
  • Drizzle v1.0 is still RC. v0.45.2 is stable for production. Check the Drizzle releases page before you upgrade.

References