Claude Code for Kysely: Type-Safe SQL Query Builder — Claude Skills 360 Blog
Blog / Backend / Claude Code for Kysely: Type-Safe SQL Query Builder
Backend

Claude Code for Kysely: Type-Safe SQL Query Builder

Published: February 17, 2027
Read time: 8 min read
By: Claude Skills 360

Kysely is a type-safe SQL query builder for TypeScript — it knows the shape of every result based on your database schema type, not an ORM abstraction. db.selectFrom("orders").select(["id","status","total_cents"]).where("customer_id","=",customerId).execute() returns { id: string; status: string; total_cents: number }[] — TypeScript errors if you select a non-existent column. The Database interface defines all table schemas. Codegen derives this interface from an existing database. Joins produce correctly typed merged results. The transaction API provides the same fluent builder within a serialized transaction. Migrations use the Migrator class with up/down functions. sql template tag escapes raw SQL safely. Kysely is a peer to Drizzle — both avoid the ORM model but Kysely stays closer to SQL syntax. Claude Code generates Kysely database types, query builders, migration files, and the full CRUD patterns matching your PostgreSQL or MySQL schema.

CLAUDE.md for Kysely Projects

## Kysely Stack
- Version: kysely >= 0.27, kysely-codegen for schema-to-types
- Dialect: PostgresDialect (pg), MysqlDialect (mysql2), SqliteDialect (better-sqlite3)
- Types: Database interface with table types — runs Codegen to derive from existing DB
- Queries: db.selectFrom("table").select([...]).where(col, op, val).execute()
- Joins: innerJoin/leftJoin with typed on condition and merged result type
- Tx: db.transaction().execute(async (trx) => {...}) — same API as db
- Migrations: new Migrator({ db, provider }) — up(db) / down(db) functions
- Raw: sql`SELECT ${sql.raw(col)} FROM orders WHERE id = ${id}` — parameterized safe

Database Type and Setup

// lib/database.ts — Kysely setup with types
import { Kysely, PostgresDialect } from "kysely"
import { Pool } from "pg"

// Database interface: one type per table
// Kysely uses Insertable<T>, Selectable<T>, Updateable<T> wrappers
interface Orders {
  id: string
  customer_id: string
  status: "pending" | "processing" | "shipped" | "delivered" | "cancelled"
  total_cents: number
  items: unknown  // JSONB — type in application code
  tracking_number: string | null
  created_at: Date
  updated_at: Date
}

interface Customers {
  id: string
  email: string
  name: string
  plan: "free" | "pro" | "enterprise"
  stripe_customer_id: string | null
  created_at: Date
}

interface Products {
  id: string
  slug: string
  name: string
  description: string | null
  price_cents: number
  stock: number
  active: boolean
}

// Export Database interface for Kysely
export interface Database {
  orders: Orders
  customers: Customers
  products: Products
}

const pool = new Pool({
  connectionString: process.env.DATABASE_URL!,
  max: 20,
  idleTimeoutMillis: 30_000,
})

export const db = new Kysely<Database>({
  dialect: new PostgresDialect({ pool }),
})

Queries

// lib/queries/orders.ts — Kysely query examples
import { db, Database } from "../database"
import { sql } from "kysely"
import type { Insertable, Updateable } from "kysely"

interface OrderItem {
  productId: string
  name: string
  quantity: number
  priceCents: number
}

export async function listCustomerOrders(
  customerId: string,
  opts: { status?: string; limit?: number; offset?: number } = {}
) {
  const { status, limit = 20, offset = 0 } = opts

  let query = db
    .selectFrom("orders")
    .select([
      "id",
      "status",
      "total_cents",
      "created_at",
    ])
    .where("customer_id", "=", customerId)
    .orderBy("created_at", "desc")
    .limit(limit)
    .offset(offset)

  if (status) {
    query = query.where("status", "=", status as Orders["status"])
  }

  return query.execute()
}

export async function getOrderWithCustomer(orderId: string) {
  return db
    .selectFrom("orders")
    .innerJoin("customers", "customers.id", "orders.customer_id")
    .select([
      "orders.id",
      "orders.status",
      "orders.total_cents",
      "orders.created_at",
      "customers.email as customer_email",
      "customers.name as customer_name",
      "customers.plan as customer_plan",
    ])
    .where("orders.id", "=", orderId)
    .executeTakeFirst()
  // Returns: { id, status, total_cents, created_at, customer_email, customer_name, customer_plan } | undefined
}

export async function createOrder(
  customerId: string,
  items: OrderItem[],
  totalCents: number
) {
  const id = crypto.randomUUID()

  return db
    .insertInto("orders")
    .values({
      id,
      customer_id: customerId,
      status: "pending",
      total_cents: totalCents,
      items: JSON.stringify(items),
      created_at: new Date(),
      updated_at: new Date(),
    } satisfies Insertable<Database["orders"]>)
    .returningAll()
    .executeTakeFirstOrThrow()
}

export async function updateOrderStatus(
  orderId: string,
  status: Orders["status"]
) {
  return db
    .updateTable("orders")
    .set({
      status,
      updated_at: new Date(),
    } satisfies Updateable<Database["orders"]>)
    .where("id", "=", orderId)
    .returningAll()
    .executeTakeFirstOrThrow()
}

// Revenue stats with raw SQL for aggregates
export async function getRevenueStats(startDate: Date, endDate: Date) {
  const result = await db
    .selectFrom("orders")
    .select([
      db.fn.count<number>("id").as("order_count"),
      db.fn.sum<number>("total_cents").as("total_revenue_cents"),
      db.fn.avg<number>("total_cents").as("avg_order_cents"),
    ])
    .where("created_at", ">=", startDate)
    .where("created_at", "<=", endDate)
    .where("status", "!=", "cancelled")
    .executeTakeFirstOrThrow()

  return {
    orderCount: result.order_count,
    totalRevenueCents: result.total_revenue_cents ?? 0,
    avgOrderCents: result.avg_order_cents ?? 0,
  }
}

Transactions

// lib/queries/checkout.ts — Kysely transaction
export async function processCheckout(
  customerId: string,
  items: OrderItem[]
) {
  return db.transaction().execute(async (trx) => {
    // All operations within trx are atomic

    // 1. Check and reserve stock
    for (const item of items) {
      const product = await trx
        .selectFrom("products")
        .select(["id", "stock", "price_cents"])
        .where("id", "=", item.productId)
        .forUpdate()  // Row-level lock
        .executeTakeFirstOrThrow()

      if (product.stock < item.quantity) {
        throw new Error(`Insufficient stock for product ${item.productId}`)
      }

      await trx
        .updateTable("products")
        .set({ stock: product.stock - item.quantity })
        .where("id", "=", item.productId)
        .execute()
    }

    // 2. Create order
    const totalCents = items.reduce(
      (sum, item) => sum + item.priceCents * item.quantity,
      0
    )

    const order = await trx
      .insertInto("orders")
      .values({
        id: crypto.randomUUID(),
        customer_id: customerId,
        status: "pending",
        total_cents: totalCents,
        items: JSON.stringify(items),
        created_at: new Date(),
        updated_at: new Date(),
      })
      .returningAll()
      .executeTakeFirstOrThrow()

    return order
  })
}

Migrations

// migrations/001_create_orders.ts
import type { Kysely } from "kysely"

export async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable("orders")
    .addColumn("id", "uuid", col => col.primaryKey().defaultTo(sql`gen_random_uuid()`))
    .addColumn("customer_id", "uuid", col => col.notNull().references("customers.id"))
    .addColumn("status", "text", col => col.notNull().defaultTo("pending"))
    .addColumn("total_cents", "integer", col => col.notNull())
    .addColumn("items", "jsonb", col => col.notNull())
    .addColumn("tracking_number", "text")
    .addColumn("created_at", "timestamptz", col =>
      col.notNull().defaultTo(sql`now()`)
    )
    .addColumn("updated_at", "timestamptz", col =>
      col.notNull().defaultTo(sql`now()`)
    )
    .execute()

  await db.schema
    .createIndex("orders_customer_id_idx")
    .on("orders")
    .column("customer_id")
    .execute()

  await db.schema
    .createIndex("orders_status_idx")
    .on("orders")
    .column("status")
    .execute()
}

export async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable("orders").execute()
}
// lib/migrate.ts — run migrations programmatically
import { FileMigrationProvider, Migrator } from "kysely"
import { db } from "./database"
import { promises as fs } from "fs"
import * as path from "path"

const migrator = new Migrator({
  db,
  provider: new FileMigrationProvider({
    fs,
    path,
    migrationFolder: path.join(__dirname, "../../migrations"),
  }),
})

// Run all pending migrations
export async function migrateToLatest() {
  const { error, results } = await migrator.migrateToLatest()

  results?.forEach(r => {
    console.log(`${r.status}: ${r.migrationName}`)
  })

  if (error) {
    console.error("Migration failed", error)
    process.exit(1)
  }
}

For the Drizzle ORM alternative that also generates types from schema definitions but with a more ORM-like query syntax and schema-as-code approach instead of Kysely’s type-interface approach, see the Drizzle guide for schema definition and migrations. For the Prisma ORM alternative when automatic migrations, Prisma Studio, and the broader Prisma ecosystem (Accelerate, Pulse) are needed alongside stronger abstraction over raw SQL, see the Prisma guide for schema and query patterns. The Claude Skills 360 bundle includes Kysely skill sets covering type setup, query building, and migrations. Start with the free tier to try Kysely query generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free