Claude Code for Kysely — Workflow Guide
The Setup
You are using Kysely, the type-safe SQL query builder for TypeScript that generates SQL without the abstraction overhead of a full ORM. Kysely gives you full SQL power with TypeScript autocompletion and compile-time type checking. Claude Code can write Kysely queries and migrations, but it defaults to Knex.js syntax or full ORM patterns.
What Claude Code Gets Wrong By Default
-
Writes Knex.js query syntax. Claude uses
knex('users').where('id', 1)string-based column references. Kysely usesdb.selectFrom('users').where('id', '=', 1)with explicit operator arguments and typed column names. -
Uses an ORM-style relation API. Claude writes
User.findOne({ include: ['posts'] })eager loading. Kysely is a query builder, not an ORM — joins are explicit SQL joins:db.selectFrom('users').innerJoin('posts', 'posts.userId', 'users.id'). -
Skips the Database type interface. Claude queries without defining the TypeScript Database interface. Kysely’s type safety comes from a
Databaseinterface that maps table names to column types — without it, you lose all autocompletion. -
Uses string interpolation in queries. Claude concatenates values into SQL strings. Kysely uses parameterized queries by default — pass values as method arguments, never interpolate them into query strings.
The CLAUDE.md Configuration
# Kysely Query Builder Project
## Architecture
- Query Builder: Kysely (NOT Knex, NOT Prisma)
- Database: PostgreSQL with pg driver
- Types: Database interface in types/database.ts
- Migrations: Kysely migrator with TypeScript migrations
## Kysely Rules
- Define Database interface mapping tables to column types
- Queries: db.selectFrom('table').select(['col1', 'col2'])
- Where clauses need operator: .where('col', '=', value)
- Joins are explicit: .innerJoin('table', 'a.id', 'b.aId')
- Insert: db.insertInto('table').values({ ... }).returning('id')
- Update: db.updateTable('table').set({ ... }).where(...)
- Never use string interpolation — all values are parameterized
- Use $castTo<Type>() for complex query result types
## Conventions
- Database type in src/types/database.ts
- Query functions in src/db/queries/ directory
- Migrations in src/db/migrations/ with timestamp prefix
- Use Kysely CLI: kysely migrate:latest
- Transactions: db.transaction().execute(async (trx) => { ... })
- Complex queries: use the expression builder (eb) for subqueries
Workflow Example
You want to create a complex query with joins and aggregation. Prompt Claude Code:
“Write a Kysely query that gets all users with their post count and most recent post date. Filter to users who have at least 5 posts. Order by post count descending.”
Claude Code should produce a query using db.selectFrom('users') with a left join to a subquery that aggregates posts by userId, using eb.fn.count() for the count, eb.fn.max() for latest date, and a having clause for the minimum post count, all with proper TypeScript types inferred from the Database interface.
Common Pitfalls
-
Forgetting the
execute()orexecuteTakeFirst()call. Claude builds queries but forgets to execute them. Kysely queries are lazy — they return a query builder until you call.execute()(array) or.executeTakeFirst()(single row or undefined). -
Type narrowing after
executeTakeFirst. Claude uses the result without null checking.executeTakeFirst()returnsT | undefined. Always check for undefined before accessing properties, or useexecuteTakeFirstOrThrow()if the row must exist. -
Migration file naming. Claude creates migration files with descriptive names like
add-users-table.ts. Kysely’s migrator processes files in alphabetical order, so prefix with timestamps (20260418_add_users_table.ts) to ensure correct ordering.