Datenbank & SQL

Kysely mit Claude Code: Typsicheres SQL für TypeScript 2026

Schema-Definition, Queries, Joins, Transactions und Migrations — Claude Code als Kysely-Experte für typsicheres SQL ohne ORM-Overhead.

📅 5. Mai 2026 ⏱ 11 min Lesezeit 🗄️ Datenbank & SQL

TypeScript-Entwickler stehen oft vor demselben Dilemma: ORMs wie Prisma bieten Komfort, schränken aber Flexibilität ein. Raw-SQL verliert die Typsicherheit. Kysely löst dieses Dilemma elegant — es ist ein vollständig typsicherer SQL Query Builder, der dir die volle SQL-Kontrolle belässt und gleichzeitig Autocomplete sowie Compile-Zeit-Fehler liefert.

Kombiniert mit Claude Code wird Kysely noch mächtiger: Der KI-Assistent kennt das gesamte Kysely-API, schlägt korrekte Typen vor, erkennt Fehler in Join-Ausdrücken und generiert vollständige Migrations-Skripte auf Knopfdruck. In diesem Artikel zeigen wir dir, wie du beide Werkzeuge optimal einsetzt.

Tipp: Kysely 0.27+ ist stabil und produktionsreif. PostgreSQL, MySQL und SQLite werden nativ unterstützt. Claude Code kennt alle Dialekte inklusive der neuesten Plugin-API.

Setup Setup und Schema-Definition

Kysely erfordert keine Code-Generierung und kein separates Schema-File. Das gesamte Datenbankschema wird direkt als TypeScript-Interface definiert — Claude Code kann aus einem bestehenden SQL-Dump oder einer Migrations-Datei automatisch das passende Interface erzeugen.

Installation

# Installation für PostgreSQL npm install kysely pg npm install -D @types/pg # Alternativ für MySQL npm install kysely mysql2 # Alternativ für SQLite npm install kysely better-sqlite3 npm install -D @types/better-sqlite3

Database Interface definieren

Das Herzstück von Kysely ist das Database-Interface. Jede Tabelle wird als eigenes Interface beschrieben — mit exakten TypeScript-Typen für jede Spalte. Claude Code generiert dieses Interface aus deinem SQL-Schema automatisch.

// src/db/types.ts import { ColumnType, Generated, Insertable, Selectable, Updateable } from 'kysely' // Tabellen-Interface: vollständig typisiert export interface UserTable { id: Generated<number> // AUTO_INCREMENT / SERIAL email: string name: string role: 'admin' | 'user' | 'viewer' created_at: ColumnType<Date, string | undefined, never> updated_at: ColumnType<Date, string | undefined, string> is_active: ColumnType<boolean, boolean | undefined, boolean> } export interface PostTable { id: Generated<number> user_id: number title: string content: string | null status: 'draft' | 'published' | 'archived' view_count: ColumnType<number, number | undefined, number> published_at:Date | null created_at: ColumnType<Date, string | undefined, never> } export interface CommentTable { id: Generated<number> post_id: number user_id: number body: string created_at: ColumnType<Date, string | undefined, never> } // Das zentrale Database-Interface export interface Database { users: UserTable posts: PostTable comments: CommentTable } // Hilfs-Typen für CRUD-Operationen export type User = Selectable<UserTable> export type NewUser = Insertable<UserTable> export type UpdateUser = Updateable<UserTable> export type Post = Selectable<PostTable> export type NewPost = Insertable<PostTable> export type UpdatePost = Updateable<PostTable>

Datenbankverbindung mit PostgresDialect

// src/db/index.ts import { Kysely, PostgresDialect, CamelCasePlugin } from 'kysely' import { Pool } from 'pg' import type { Database } from './types' // Connection Pool erstellen const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, }) // Kysely-Instanz — einmal erstellen, überall nutzen export const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool }), plugins: [ new CamelCasePlugin(), // snake_case DB ↔ camelCase TS ], log(event) { if (event.level === 'query') { console.log(event.query.sql) // SQL loggen console.log(event.query.parameters) // Parameterwerte } } }) // Graceful Shutdown process.on('SIGTERM', async () => { await db.destroy() })
Claude Code Prompt: "Erstelle ein Kysely Database Interface für meine PostgreSQL-Tabelle mit dieser CREATE TABLE Anweisung: [SQL einfügen]" — Claude generiert alle Spaltentypen korrekt inklusive Nullable-Handling und ColumnType-Wrapper.

Weitere Dialekte

// MySQL import { MysqlDialect } from 'kysely' import { createPool } from 'mysql2' const dialect = new MysqlDialect({ pool: createPool({ uri: process.env.DATABASE_URL }) }) // SQLite (z.B. für Tests oder Edge-Deployments) import { SqliteDialect } from 'kysely' import Database from 'better-sqlite3' const dialect = new SqliteDialect({ database: new Database('./dev.db') })

Query Grundlegende Queries

Kysely's Builder-API spiegelt SQL fast 1:1 wider — wer SQL kann, lernt Kysely in Minuten. Claude Code kennt alle Builder-Methoden und warnt, wenn du eine Spalte verwendest, die im Schema nicht existiert.

SELECT mit where, orderBy und limit

// src/db/queries/users.ts import { db } from '../index' import type { User } from '../types' // Einen User per ID laden export async function getUserById(id: number): Promise<User | undefined> { return db .selectFrom('users') .selectAll() .where('id', '=', id) .executeTakeFirst() } // Aktive User mit Pagination export async function getActiveUsers( page: number = 1, pageSize: number = 20 ): Promise<User[]> { return db .selectFrom('users') .selectAll() .where('isActive', '=', true) .orderBy('createdAt', 'desc') .limit(pageSize) .offset((page - 1) * pageSize) .execute() } // Spezifische Spalten selektieren export async function getUserEmails() { return db .selectFrom('users') .select(['id', 'email', 'name']) .where('role', '=', 'admin') .execute() // Rückgabetyp: { id: number; email: string; name: string }[] }

INSERT, UPDATE und DELETE

import type { NewUser, UpdateUser } from '../types' // INSERT mit RETURNING export async function createUser(data: NewUser): Promise<User> { const result = await db .insertInto('users') .values(data) .returningAll() // PostgreSQL: RETURNING * .executeTakeFirstOrThrow() return result } // Batch-INSERT export async function createManyUsers(users: NewUser[]) { return db .insertInto('users') .values(users) .execute() } // UPDATE export async function updateUser( id: number, data: UpdateUser ): Promise<User | undefined> { return db .updateTable('users') .set(data) .where('id', '=', id) .returningAll() .executeTakeFirst() } // DELETE export async function deleteUser(id: number): Promise<void> { await db .deleteFrom('users') .where('id', '=', id) .execute() }

Komplexe WHERE-Klauseln

import { sql } from 'kysely' // OR-Bedingungen mit expressionBuilder export async function searchUsers(query: string) { return db .selectFrom('users') .selectAll() .where((eb) => eb.or([ eb('email', 'like', `%${query}%`), eb('name', 'like', `%${query}%`), ]) ) .where('isActive', '=', true) .execute() } // IN-Abfrage export async function getUsersByIds(ids: number[]) { return db .selectFrom('users') .selectAll() .where('id', 'in', ids) .execute() } // Raw SQL für komplexe Ausdrücke export async function getRecentUsers(days: number) { return db .selectFrom('users') .selectAll() .where('createdAt', '>=', sql<Date>`NOW() - INTERVAL '${sql.lit(days)} days'` ) .execute() }
Achtung: Nutze sql.lit() oder Kysely-Parameter für alle User-Inputs — niemals String-Interpolation direkt in sql``. Claude Code erkennt potenzielle SQL-Injection-Stellen und warnt automatisch.

Join Joins und Relations

Joins in Kysely sind vollständig typsicher — TypeScript weiß nach einem innerJoin exakt, welche Spalten aus welcher Tabelle verfügbar sind. Claude Code hilft, komplexe Multi-Table-Joins korrekt zu konstruieren und Namenskonflikte zu lösen.

innerJoin — Posts mit User-Daten

import { db } from '../index' // Posts mit Autor-Informationen export async function getPostsWithAuthor() { return db .selectFrom('posts') .innerJoin('users', 'users.id', 'posts.userId') .select([ 'posts.id', 'posts.title', 'posts.status', 'posts.createdAt', 'users.name as authorName', 'users.email as authorEmail', ]) .where('posts.status', '=', 'published') .orderBy('posts.createdAt', 'desc') .execute() // Typ: { id, title, status, createdAt, authorName, authorEmail }[] }

leftJoin — Posts auch ohne Kommentare

// LEFT JOIN: Posts + Kommentaranzahl (auch Posts ohne Kommentare) export async function getPostsWithCommentCount() { return db .selectFrom('posts') .leftJoin('comments', 'comments.postId', 'posts.id') .select((eb) => [ 'posts.id', 'posts.title', 'posts.viewCount', eb.fn.count('comments.id').as('commentCount'), ]) .groupBy(['posts.id', 'posts.title', 'posts.viewCount']) .orderBy('posts.viewCount', 'desc') .execute() }

Alias-Tabellen für Self-Joins und komplexe Queries

import { db } from '../index' // aliasTable: für Self-Joins oder mehrfach gejoinede Tabellen export async function getPostsWithDetails(userId: number) { const author = db.dynamic.ref('author') return db .selectFrom('posts') .innerJoin( 'users as author', 'author.id', 'posts.userId' ) .leftJoin('comments', 'comments.postId', 'posts.id') .leftJoin( 'users as commenter', 'commenter.id', 'comments.userId' ) .select([ 'posts.id as postId', 'posts.title', 'author.name as authorName', 'comments.body as commentBody', 'commenter.name as commenterName', ]) .where('posts.userId', '=', userId) .execute() } // Subquery als Join export async function getTopPostsByUser() { const subquery = db .selectFrom('posts') .select(eb => [ 'userId', eb.fn.max('viewCount').as('maxViews') ]) .groupBy('userId') .as('topPosts') return db .selectFrom(['users', subquery]) .select(['users.name', 'topPosts.maxViews']) .whereRef('users.id', '=', 'topPosts.userId') .execute() }
Claude Code Vorteil: Bei komplexen Joins mit mehreren Tabellen-Aliases kann Claude Code den gesamten Typen-Stack ableiten und zeigt dir, welche Spalten nach dem Join verfügbar sind — ohne dass du die Kysely-Dokumentation nachschlagen musst.

Transaction Transactions

Kysely unterstützt Transactions nativ über db.transaction(). Alle Operationen innerhalb eines Callbacks laufen in einer gemeinsamen Transaktion — bei einem Fehler wird automatisch ein Rollback durchgeführt.

Einfache Transaction

import { db } from '../index' import type { NewPost, NewUser } from '../types' // User + ersten Post in einer Transaktion erstellen export async function createUserWithPost( userData: NewUser, postData: Omit<NewPost, 'userId'> ) { return db.transaction().execute(async (trx) => { // Schritt 1: User anlegen const user = await trx .insertInto('users') .values(userData) .returningAll() .executeTakeFirstOrThrow() // Schritt 2: Post mit User-ID anlegen const post = await trx .insertInto('posts') .values({ ...postData, userId: user.id }) .returningAll() .executeTakeFirstOrThrow() // Fehler hier → automatischer Rollback für BEIDE Inserts return { user, post } }) }

Transaction mit manuellem Rollback

import { TransactionBuilder } from 'kysely' // Transfer-Funktion mit expliziter Fehlerbehandlung export async function transferCredits( fromUserId: number, toUserId: number, amount: number ): Promise<{ success: boolean; error?: string }> { try { await db.transaction().execute(async (trx) => { // Sender-Balance prüfen const sender = await trx .selectFrom('users') .select(['id', 'credits']) .where('id', '=', fromUserId) .forUpdate() // SELECT FOR UPDATE — Row-Lock .executeTakeFirstOrThrow() if (sender.credits < amount) { throw new Error('Insufficient credits') } // Sender abziehen await trx .updateTable('users') .set((eb) => ({ credits: eb('credits', '-', amount) })) .where('id', '=', fromUserId) .execute() // Empfänger gutschreiben await trx .updateTable('users') .set((eb) => ({ credits: eb('credits', '+', amount) })) .where('id', '=', toUserId) .execute() }) return { success: true } } catch (error) { // Kysely rollback'd automatisch bei Exception return { success: false, error: (error as Error).message } } }

Isolation Level festlegen

// Serializable Isolation für kritische Transaktionen await db .transaction() .setIsolationLevel('serializable') .execute(async (trx) => { // Alle Reads/Writes sind serialisierbar })
Wichtig: Übergib die trx-Instanz an alle Hilfsfunktionen, die in derselben Transaktion laufen sollen — nicht die globale db-Instanz. Claude Code erkennt diesen Fehler und zeigt einen Typ-Fehler, wenn du versehentlich db statt trx verwendest.

Migration Migrations

Kysely bringt ein einfaches Migrations-System mit: Migrator und FileMigrationProvider. Jede Migration ist eine TypeScript-Datei mit up und down-Funktion. Claude Code kann komplette Migrations-Dateien aus Tabellenbeschreibungen generieren.

Migration-Datei erstellen

// src/db/migrations/2026_05_01_create_users.ts import { Kysely, sql } from 'kysely' export async function up(db: Kysely<any>): Promise<void> { await db.schema .createTable('users') .addColumn('id', 'serial', (col) => col.primaryKey()) .addColumn('email', 'varchar(255)', (col) => col.notNull().unique()) .addColumn('name', 'varchar(100)', (col) => col.notNull()) .addColumn('role', 'varchar(20)', (col) => col.notNull().defaultTo('user')) .addColumn('is_active', 'boolean', (col) => col.notNull().defaultTo(true)) .addColumn('created_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`) ) .addColumn('updated_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`) ) .execute() // Index auf email await db.schema .createIndex('idx_users_email') .on('users') .column('email') .execute() } export async function down(db: Kysely<any>): Promise<void> { await db.schema.dropTable('users').execute() }

Posts-Migration mit Foreign Key

// src/db/migrations/2026_05_02_create_posts.ts import { Kysely, sql } from 'kysely' export async function up(db: Kysely<any>): Promise<void> { await db.schema .createTable('posts') .addColumn('id', 'serial', (col) => col.primaryKey()) .addColumn('user_id', 'integer', (col) => col.notNull().references('users.id').onDelete('cascade') ) .addColumn('title', 'varchar(255)', (col) => col.notNull()) .addColumn('content', 'text') .addColumn('status', 'varchar(20)', (col) => col.notNull().defaultTo('draft')) .addColumn('view_count', 'integer', (col) => col.notNull().defaultTo(0)) .addColumn('published_at', 'timestamptz') .addColumn('created_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`) ) .execute() await db.schema .createIndex('idx_posts_user_id') .on('posts') .column('user_id') .execute() } export async function down(db: Kysely<any>): Promise<void> { await db.schema.dropTable('posts').execute() }

Migrator konfigurieren und ausführen

// src/db/migrate.ts import { FileMigrationProvider, Migrator } from 'kysely' import * as path from 'path' import * as fs from 'fs' import { db } from './index' const migrator = new Migrator({ db, provider: new FileMigrationProvider({ fs, path, migrationFolder: path.join(__dirname, 'migrations'), }), }) async function migrateToLatest() { const { error, results } = await migrator.migrateToLatest() results?.forEach((it) => { if (it.status === 'Success') { console.log(`✅ Migration "${it.migrationName}" erfolgreich`) } else if (it.status === 'Error') { console.error(`❌ Migration "${it.migrationName}" fehlgeschlagen`) } }) if (error) { console.error('Migration fehlgeschlagen:', error) process.exit(1) } await db.destroy() } // Rollback einzelner Migration async function rollbackLast() { const { error, results } = await migrator.migrateDown() // ... analog zu oben await db.destroy() } migrateToLatest()
# package.json Scripts { "scripts": { "migrate": "tsx src/db/migrate.ts", "migrate:down": "tsx src/db/rollback.ts" } }
Naming Convention: Migrations-Dateien müssen alphabetisch sortierbar sein — verwende ISO-Datum-Prefix (2026_05_01_). Kysely führt Migrations in dieser Reihenfolge aus. Claude Code generiert den korrekten Dateinamen automatisch.

Vergleich Kysely vs Prisma vs Drizzle

Die Wahl des richtigen SQL-Tools hängt vom Projekt ab. Hier ein ehrlicher Vergleich der drei populärsten TypeScript-Optionen — mit Blick auf Claude Code-Kompatibilität und produktive KI-unterstützte Entwicklung.

Kriterium Kysely Prisma Drizzle
Typsicherheit Vollständig (Interface-basiert) Vollständig (Generated) Vollständig (Schema-First)
SQL-Kontrolle Vollständig (1:1 SQL) Eingeschränkt (Abstrahiert) Sehr hoch
Code-Generierung nötig Nein Ja (prisma generate) Optional (Introspection)
Bundle-Größe Sehr klein (~50kB) Groß (Engines ~50MB) Klein (~30kB)
Edge Runtime Support Ja Eingeschränkt Ja (Cloudflare D1 etc.)
Migrations-System Eingebaut (einfach) Vollständig Vollständig (Kit)
Raw SQL einfügbar sql`` Template Tag $queryRaw sql`` Template Tag
Lernkurve Flach (SQL-Kenntnisse reichen) Mittel (eigene Konzepte) Mittel (Schema-DSL)
Claude Code DX Exzellent (kein Codegen nötig) Gut (nach generate) Sehr gut
Relations/Eager Loading Manuell via Join Automatisch (include) Manuell oder relational queries
PostgreSQL Dialekt Voll unterstützt Voll unterstützt Voll unterstützt
Performance (Overhead) Minimal Moderate (Binary Engine) Minimal

Wann welches Tool?

Claude Code Empfehlung: Für Projekte mit komplexen SQL-Anforderungen, bestehenden Datenbanken oder Edge-Deployments ist Kysely der beste Startpunkt — Claude Code kennt das gesamte API und kann ohne Code-Generierungsschritt direkt typsichere Queries schreiben. Für Greenfield-Projekte mit einfachen Relations ist Prisma oft schneller zu starten.

Migration von Prisma zu Kysely

Claude Code kann Prisma-Schemas direkt in Kysely-Interfaces konvertieren. Der Workflow: Prisma-Schema zeigen → Claude generiert das Database-Interface und alle Query-Äquivalente. Migrationszeit für mittlere Projekte: typischerweise 2–4 Stunden.

// Prisma Schema → Claude Code Prompt // "Konvertiere dieses Prisma-Schema in ein Kysely Database Interface // und schreibe die äquivalenten findMany/findUnique Queries in Kysely:" // Prisma: prisma.user.findMany({ where: { isActive: true } }) // Kysely: db.selectFrom('users').selectAll().where('isActive', '=', true).execute() // Prisma: prisma.post.findUnique({ where: { id }, include: { author: true } }) // Kysely: db.selectFrom('posts') // .innerJoin('users', 'users.id', 'posts.userId') // .select(['posts.id', 'posts.title', 'users.name as authorName']) // .where('posts.id', '=', id) // .executeTakeFirst()

Fazit: Kysely + Claude Code = typsicheres SQL ohne Kompromisse

Kysely ist kein ORM — und das ist seine größte Stärke. Du schreibst echtes SQL in TypeScript, bekommst volle Autocomplete-Unterstützung und Compile-Zeit-Fehler, ohne auf Code-Generierung oder Schema-Dateien angewiesen zu sein.

Claude Code macht Kysely noch produktiver: Der Assistent kennt die gesamte API, erkennt Type-Fehler in komplexen Joins, generiert Migrations-Dateien aus SQL-Dumps und erklärt, wann executeTakeFirst vs. executeTakeFirstOrThrow die richtige Wahl ist.

Ob du PostgreSQL, MySQL oder SQLite nutzt — Kysely passt sich dem Dialekt an, während dein TypeScript-Code identisch bleibt. Für Teams, die SQL beherrschen und typsichere Queries ohne ORM-Overhead wollen, ist Kysely 2026 die erste Wahl.

Nächste Schritte: Starte mit npm install kysely pg, definiere dein Database-Interface, und lass Claude Code die ersten Queries für dein bestehendes Schema generieren. In unter 30 Minuten hast du typsichere SQL-Abfragen in Production-Qualität.

Claude Code für dein TypeScript-Projekt nutzen

Typsichere Queries, automatische Migrations, komplexe Joins — Claude Code kennt Kysely in- und auswendig und beschleunigt deine Datenbankentwicklung erheblich.

Jetzt kostenlos testen →