Skip to Content
Overview

Last Updated: 3/20/2026


Overview

Kysely (pronounced “Key-Seh-Lee”) is a type-safe, autocompletion-friendly TypeScript SQL query builder. Unlike traditional ORMs that abstract away SQL with their own query languages and runtime magic, Kysely embraces SQL while adding compile-time safety through TypeScript’s type system.

What Makes Kysely Different

Kysely is not an ORM. It doesn’t map database rows to class instances, manage object graphs, or track changes. Instead, it’s a query builder that generates SQL strings and provides TypeScript types that match your database schema. You write queries that look and feel like SQL, but with full IDE autocompletion and compile-time validation.

The key difference from raw SQL: every table reference, column name, and join condition is validated against your database schema at compile time. If you try to select a column that doesn’t exist, or join tables on incompatible types, TypeScript will catch the error before you run the code.

The key difference from ORMs: Kysely has no runtime overhead for object mapping, no lazy loading pitfalls, and no hidden queries. The SQL you see in your code is the SQL that runs. This makes performance predictable and debugging straightforward.

The Immutable Query Builder Pattern

Every Kysely query builder method returns a new immutable instance. This design, inspired by Knex.js, means you can safely compose and reuse query fragments without side effects.

const baseQuery = db.selectFrom('person').select('id') // These are independent queries - baseQuery is unchanged const adults = baseQuery.where('age', '>=', 18) const minors = baseQuery.where('age', '<', 18)

This immutability is implemented in the Kysely class (see src/kysely.ts) and all query builder classes. Methods like withPlugin, withSchema, and withTables return new instances rather than mutating the original. The same pattern applies to SelectQueryBuilder, InsertQueryBuilder, UpdateQueryBuilder, and DeleteQueryBuilder — every filtering, joining, or modification method creates a fresh builder.

The immutability extends to the internal query node tree. When you call .where(), Kysely clones the underlying SelectQueryNode and adds a new WhereNode to the clone, leaving the original untouched.

Runtime Environments

Kysely runs anywhere JavaScript runs. The core library has zero dependencies and works in:

  • Node.js (minimum version 20.0.0, as specified in package.json)
  • Deno
  • Bun
  • Cloudflare Workers
  • Web browsers (when paired with a browser-compatible database driver)

Each environment uses the same Kysely API. The only difference is the database driver you provide to the dialect. For example, PostgreSQL uses the pg driver in Node.js, but you could use a different driver in Deno or a serverless environment.

How Type Safety Works

Kysely’s type safety comes from a TypeScript Database interface you define. This interface maps table names to table schema interfaces:

interface Database { person: PersonTable pet: PetTable } interface PersonTable { id: Generated<number> first_name: string last_name: string | null }

When you write db.selectFrom('person'), TypeScript knows which columns are available. When you write .select('first_name'), TypeScript validates that first_name exists on the person table and infers the result type as { first_name: string }.

This type inference works through complex queries: subqueries, joins, CTEs, and even aliased columns. Kysely parses string literals like 'pet.name as pet_name' at the type level and adds pet_name to the result type. This is implemented using advanced TypeScript features like template literal types and conditional types (see src/parser/select-parser.ts and related type utilities in src/util/type-utils.ts).

The Kysely class itself is generic over your Database type, and every query builder method preserves and refines this type information as you build the query.

Escape Hatches for Dynamic Queries

Type safety is powerful, but sometimes you need to bypass it. Kysely provides two main escape hatches:

The sql template tag lets you write raw SQL fragments with parameter binding:

import { sql } from 'kysely' const result = await db .selectFrom('person') .select(sql<string>`concat(first_name, ' ', last_name)`.as('full_name')) .execute()

You provide the result type (<string>) manually, and Kysely trusts you. The sql tag handles parameter escaping and works anywhere an expression is expected.

The DynamicModule (accessed via db.dynamic) provides utilities for building queries with runtime-determined table and column names. This is useful when column names come from user input or configuration files. See src/dynamic/dynamic.js for the implementation.

Architecture: Dialect, Driver, Compiler, Adapter

Kysely’s architecture separates concerns into four components, all defined in the Dialect interface (see src/dialect/dialect.ts):

  1. Dialect — The top-level factory that creates the other three components. Examples: PostgresDialect, MysqlDialect, SqliteDialect, MssqlDialect (see src/dialect/*/).

  2. Driver — Manages the connection pool and executes compiled queries. Each dialect provides its own driver that wraps the underlying third-party library (pg, mysql2, better-sqlite3, tedious).

  3. QueryCompiler — Transforms Kysely’s internal query node tree into a SQL string. Different dialects have different SQL syntax (e.g., PostgreSQL uses $1 placeholders, MySQL uses ?), so each dialect has its own compiler.

  4. DialectAdapter — Handles dialect-specific behaviors like whether the database supports RETURNING clauses, transactional DDL, or CREATE IF NOT EXISTS. The adapter also normalizes result metadata (like insertId for MySQL vs. PostgreSQL).

When you instantiate new Kysely({ dialect }), the constructor calls dialect.createDriver(), dialect.createQueryCompiler(), and dialect.createAdapter() to build the execution pipeline (see the Kysely constructor in src/kysely.ts).

Why Kysely Exists

Kysely was created to solve a specific problem: how to write SQL in TypeScript without sacrificing type safety or performance. ORMs like TypeORM and Sequelize offer convenience but come with complexity, runtime overhead, and a learning curve for their query languages. Raw SQL offers performance and clarity but no type safety.

Kysely bridges the gap. You get the performance and transparency of raw SQL with the safety and developer experience of TypeScript. The project was authored by Sami Koskimäki and is actively maintained by a core team including Igal Klebanov (see README.md for the full contributor list).