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: April 2, 2027
Read time: 8 min read
By: Claude Skills 360

Kysely is a type-safe SQL query builder for TypeScript — queries are fully typed from a database schema interface you define. db.selectFrom("orders").select(["id", "status", "totalCents"]).where("status", "=", "pending").execute() returns { id: string; status: string; totalCents: number }[] inferred from your schema. db.insertInto("orders").values({ ... }).returningAll().executeTakeFirstOrThrow() inserts and returns the new row. db.transaction().execute(async trx => { ... }) runs queries in a transaction. sql\NOW()`injects raw SQL.CamelCasePluginconverts snake_case to camelCase transparently.FileMigrationProviderwithdb.migrate.latest()runs migration files. Kysely works with PostgreSQL (viapg), SQLite (via better-sqlite3`), and MySQL. Claude Code generates Kysely schema types, query builders, join queries, transactions, and migration files for type-safe database access without an ORM.

CLAUDE.md for Kysely

## Kysely Stack
- Version: kysely >= 0.27
- Schema: interface Database { tableName: TableInterface } — column types exactly match DB
- Dialect: PostgresDialect({ pool: new Pool({ connectionString }) })
- Select: db.selectFrom("orders").select(["id", "status"]).where("id", "=", id).executeTakeFirst()
- Insert: db.insertInto("users").values({ email, name }).returningAll().executeTakeFirstOrThrow()
- Update: db.updateTable("orders").set({ status: "shipped" }).where("id", "=", id).execute()
- Join: .innerJoin("customers", "orders.customerId", "customers.id")
- Transaction: db.transaction().execute(async trx => { ... trx.insertInto(...) })
- Plugin: CamelCasePlugin() — auto snake_case ↔ camelCase
- Migrations: FileMigrationProvider from fs/path — db.migrate.latest()

Database Schema Types

// lib/db/types.ts — database interface matching your schema exactly
import type { Generated, Insertable, Selectable, Updateable, ColumnType } from "kysely"

// orders table
export interface OrderTable {
  id: Generated<string>          // AUTO-GENERATED — don't insert
  customer_id: string
  status: "pending" | "processing" | "shipped" | "delivered" | "cancelled"
  total_cents: number
  notes: string | null
  created_at: Generated<Date>    // AUTO-GENERATED
  updated_at: ColumnType<Date, never, Date>  // Never insert, update only
  deleted_at: Date | null
}

// customers table
export interface CustomerTable {
  id: Generated<string>
  email: string
  name: string
  stripe_customer_id: string | null
  created_at: Generated<Date>
}

// order_items table
export interface OrderItemTable {
  id: Generated<string>
  order_id: string
  product_id: string
  quantity: number
  unit_price_cents: number
}

// products table
export interface ProductTable {
  id: Generated<string>
  name: string
  slug: string
  price_cents: number
  stock_quantity: number
  active: boolean
  created_at: Generated<Date>
}

// Database interface — maps table names to row types
export interface Database {
  orders: OrderTable
  customers: CustomerTable
  order_items: OrderItemTable
  products: ProductTable
}

// Convenience types for CRUD operations
export type Order = Selectable<OrderTable>
export type NewOrder = Insertable<OrderTable>
export type OrderUpdate = Updateable<OrderTable>

export type Customer = Selectable<CustomerTable>
export type NewCustomer = Insertable<CustomerTable>

Kysely Client Setup

// lib/db/client.ts — Kysely with PostgreSQL
import { Kysely, PostgresDialect, CamelCasePlugin } from "kysely"
import { Pool } from "pg"
import type { Database } from "./types"

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

export const db = new Kysely<Database>({
  dialect: new PostgresDialect({ pool }),
  plugins: [
    new CamelCasePlugin(),  // snake_case columns → camelCase in TypeScript
  ],
  log(event) {
    if (process.env.NODE_ENV === "development" && event.level === "query") {
      console.log("[Kysely]", event.query.sql, event.query.parameters)
    }
  },
})

export type DB = typeof db

Typed Queries

// lib/db/order-queries.ts — type-safe queries
import { db } from "./client"
import { sql } from "kysely"

// SELECT with joins — return type inferred from .select()
export async function getOrderWithCustomer(orderId: string) {
  return db
    .selectFrom("orders")
    .innerJoin("customers", "orders.customerId", "customers.id")
    .select([
      "orders.id",
      "orders.status",
      "orders.totalCents",
      "orders.createdAt",
      "customers.name as customerName",
      "customers.email as customerEmail",
    ])
    .where("orders.id", "=", orderId)
    .where("orders.deletedAt", "is", null)
    .executeTakeFirst()
  // Returns: { id, status, totalCents, createdAt, customerName, customerEmail } | undefined
}

// Complex query with subquery and aggregation
export async function getOrdersWithItemCount(filters: {
  status?: string
  customerId?: string
  limit?: number
  offset?: number
}) {
  let query = db
    .selectFrom("orders")
    .leftJoin("orderItems", "orders.id", "orderItems.orderId")
    .select([
      "orders.id",
      "orders.status",
      "orders.totalCents",
      "orders.createdAt",
      db.fn.count<number>("orderItems.id").as("itemCount"),
    ])
    .where("orders.deletedAt", "is", null)
    .groupBy("orders.id")
    .orderBy("orders.createdAt", "desc")
    .limit(filters.limit ?? 20)
    .offset(filters.offset ?? 0)

  if (filters.status) {
    query = query.where("orders.status", "=", filters.status)
  }
  if (filters.customerId) {
    query = query.where("orders.customerId", "=", filters.customerId)
  }

  return query.execute()
}

// INSERT with returning
export async function createOrder(data: {
  customerId: string
  status?: string
  totalCents: number
  notes?: string
}) {
  return db
    .insertInto("orders")
    .values({
      customerId: data.customerId,
      status: data.status ?? "pending",
      totalCents: data.totalCents,
      notes: data.notes ?? null,
    })
    .returningAll()
    .executeTakeFirstOrThrow()
  // Throws if insert fails — returns full row with generated id and timestamps
}

// UPDATE with conditional fields
export async function updateOrderStatus(
  orderId: string,
  status: string,
  notes?: string
) {
  const result = await db
    .updateTable("orders")
    .set({
      status,
      ...(notes !== undefined && { notes }),
      updatedAt: new Date(),
    })
    .where("id", "=", orderId)
    .where("deletedAt", "is", null)
    .returning(["id", "status", "updatedAt"])
    .executeTakeFirst()

  return result ?? null
}

// Raw SQL for complex queries
export async function getRevenueByMonth(year: number) {
  return db
    .selectFrom("orders")
    .select([
      sql<string>`TO_CHAR(created_at, 'YYYY-MM')`.as("month"),
      db.fn.sum<number>("totalCents").as("totalRevenue"),
      db.fn.count<number>("id").as("orderCount"),
    ])
    .where(sql`EXTRACT(YEAR FROM created_at)`, "=", year)
    .where("status", "!=", "cancelled")
    .where("deletedAt", "is", null)
    .groupBy(sql`TO_CHAR(created_at, 'YYYY-MM')`)
    .orderBy(sql`TO_CHAR(created_at, 'YYYY-MM')`, "asc")
    .execute()
}

Transactions

// lib/db/transaction-examples.ts — multi-table transactions
import { db } from "./client"

export async function placeOrder(params: {
  customerId: string
  items: { productId: string; quantity: number }[]
}) {
  return db.transaction().execute(async trx => {
    // 1. Fetch products and check stock
    const products = await trx
      .selectFrom("products")
      .select(["id", "name", "priceCents", "stockQuantity"])
      .where("id", "in", params.items.map(i => i.productId))
      .execute()

    for (const item of params.items) {
      const product = products.find(p => p.id === item.productId)
      if (!product) throw new Error(`Product ${item.productId} not found`)
      if (product.stockQuantity < item.quantity) {
        throw new Error(`Insufficient stock for ${product.name}`)
      }
    }

    // 2. Calculate total
    const totalCents = params.items.reduce((sum, item) => {
      const product = products.find(p => p.id === item.productId)!
      return sum + product.priceCents * item.quantity
    }, 0)

    // 3. Create order
    const order = await trx
      .insertInto("orders")
      .values({ customerId: params.customerId, totalCents, status: "pending" })
      .returningAll()
      .executeTakeFirstOrThrow()

    // 4. Create order items
    await trx
      .insertInto("orderItems")
      .values(
        params.items.map(item => ({
          orderId: order.id,
          productId: item.productId,
          quantity: item.quantity,
          unitPriceCents: products.find(p => p.id === item.productId)!.priceCents,
        }))
      )
      .execute()

    // 5. Decrement stock
    for (const item of params.items) {
      await trx
        .updateTable("products")
        .set(eb => ({ stockQuantity: eb("stockQuantity", "-", item.quantity) }))
        .where("id", "=", item.productId)
        .execute()
    }

    return order
  })
}

For the Drizzle ORM alternative when a more complete ORM with relations API, schema-as-code migrations, and a higher-level db.query API for nested eager loading are preferred — Drizzle has the schema definition and migration tooling built-in while Kysely leaves migrations to SQL files, see the Drizzle Advanced guide. For the raw pg alternative when Kysely’s builder abstraction is overkill and tagged template SQL strings with full type control using @pgtyped/runtime are preferred — @pgtyped/runtime generates TypeScript types directly from .sql files without any query builder layer, see the type-safe SQL guide. The Claude Skills 360 bundle includes Kysely skill sets covering schema types, joins, transactions, and migrations. Start with the free tier to try type-safe SQL 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