DEV Community

Alex Spinov
Alex Spinov

Posted on

Kysely Has a Free Type-Safe SQL Query Builder With Zero Magic

ORMs hide SQL. Query builders expose it. Kysely is a type-safe TypeScript SQL query builder that gives you full SQL power with compile-time guarantees.

No Magic, Just Types

import { Kysely, PostgresDialect } from "kysely";
import { Pool } from "pg";

interface Database {
  users: {
    id: Generated<number>;
    name: string;
    email: string;
    created_at: Generated<Date>;
  };
  posts: {
    id: Generated<number>;
    title: string;
    author_id: number;
    published: boolean;
  };
}

const db = new Kysely<Database>({
  dialect: new PostgresDialect({ pool: new Pool({ connectionString: "..." }) }),
});
Enter fullscreen mode Exit fullscreen mode

Type-Safe Queries

// Select — every column is typed
const users = await db
  .selectFrom("users")
  .select(["id", "name", "email"])
  .where("email", "like", "%@gmail.com")
  .orderBy("created_at", "desc")
  .limit(10)
  .execute();
// Type: { id: number; name: string; email: string }[]

// Typo in column name? Compile error!
db.selectFrom("users").select("naem"); // ❌ TypeScript error

// Wrong table? Compile error!
db.selectFrom("userz"); // ❌ TypeScript error
Enter fullscreen mode Exit fullscreen mode

Joins

const postsWithAuthors = await db
  .selectFrom("posts")
  .innerJoin("users", "users.id", "posts.author_id")
  .select(["posts.title", "users.name as authorName"])
  .where("posts.published", "=", true)
  .execute();
// Type: { title: string; authorName: string }[]
Enter fullscreen mode Exit fullscreen mode

Subqueries

const prolificAuthors = await db
  .selectFrom("users")
  .select(["users.name", (eb) =>
    eb.selectFrom("posts")
      .whereRef("posts.author_id", "=", "users.id")
      .select(eb.fn.count("posts.id").as("post_count"))
      .as("post_count")
  ])
  .orderBy("post_count", "desc")
  .execute();
Enter fullscreen mode Exit fullscreen mode

Insert, Update, Delete

// Insert
const newUser = await db
  .insertInto("users")
  .values({ name: "Alice", email: "alice@test.com" })
  .returningAll()
  .executeTakeFirstOrThrow();

// Update
await db
  .updateTable("users")
  .set({ name: "Alice Updated" })
  .where("id", "=", newUser.id)
  .execute();

// Delete
await db
  .deleteFrom("posts")
  .where("published", "=", false)
  .execute();
Enter fullscreen mode Exit fullscreen mode

Migrations

import { Kysely, sql } from "kysely";

export async function up(db: Kysely<any>) {
  await db.schema
    .createTable("users")
    .addColumn("id", "serial", (col) => col.primaryKey())
    .addColumn("name", "text", (col) => col.notNull())
    .addColumn("email", "text", (col) => col.notNull().unique())
    .addColumn("created_at", "timestamp", (col) => col.defaultTo(sql`now()`))
    .execute();
}

export async function down(db: Kysely<any>) {
  await db.schema.dropTable("users").execute();
}
Enter fullscreen mode Exit fullscreen mode

Works With Every Database

  • PostgreSQL: pg, postgres.js
  • MySQL: mysql2
  • SQLite: better-sqlite3
  • MSSQL: tedious
  • PlanetScale: serverless driver
  • Cloudflare D1: @miniflare/d1

Why Kysely Over Drizzle/Prisma

Prisma Drizzle Kysely
Approach ORM SQL-like DSL SQL query builder
SQL knowledge Not needed Helpful Required
Complex queries Limited Good Excellent
Raw SQL Awkward Good Native
Bundle size ~2MB 33KB 29KB

Kysely is for developers who KNOW SQL and want TypeScript to check it.


Need database expertise for your project? I build data pipelines and scraping solutions. Email spinov001@gmail.com or explore my Apify tools.

Top comments (0)