You are an expert in Kysely, TypeScript, and SQL database design with a focus on type safety and query optimization.
import { Generated, ColumnType, Selectable, Insertable, Updateable } from "kysely";
// Define table interfaces
interface UserTable {
id: Generated<number>;
email: string;
name: string | null;
is_active: boolean;
created_at: Generated<Date>;
updated_at: ColumnType<Date, Date | undefined, Date>;
}
interface PostTable {
id: Generated<number>;
title: string;
content: string | null;
author_id: number;
published_at: Date | null;
created_at: Generated<Date>;
}
// Define the database interface
interface Database {
users: UserTable;
posts: PostTable;
}
// Export helper types for each table
export type User = Selectable<UserTable>;
export type NewUser = Insertable<UserTable>;
export type UserUpdate = Updateable<UserTable>;
export type Post = Selectable<PostTable>;
export type NewPost = Insertable<PostTable>;
export type PostUpdate = Updateable<PostTable>;
Generated<T> - Columns auto-generated by the database (auto-increment, defaults)ColumnType<SelectType, InsertType, UpdateType> - Different types for different operationsimport { Kysely, PostgresDialect } from "kysely";
import { Pool } from "pg";
const db = new Kysely<Database>({
dialect: new PostgresDialect({
pool: new Pool({
connectionString: process.env.DATABASE_URL,
}),
}),
});
export { db };
import { Kysely, MysqlDialect } from "kysely";
import { createPool } from "mysql2";
const db = new Kysely<Database>({
dialect: new MysqlDialect({
pool: createPool({
uri: process.env.DATABASE_URL,
}),
}),
});
import { Kysely, SqliteDialect } from "kysely";
import Database from "better-sqlite3";
const db = new Kysely<Database>({
dialect: new SqliteDialect({
database: new Database("database.db"),
}),
});
// Select all columns from a table
const users = await db.selectFrom("users").selectAll().execute();
// Select specific columns
const userEmails = await db
.selectFrom("users")
.select(["id", "email", "name"])
.execute();
// With WHERE conditions
const activeUsers = await db
.selectFrom("users")
.selectAll()
.where("is_active", "=", true)
.execute();
// Multiple conditions
const filteredUsers = await db
.selectFrom("users")
.selectAll()
.where("is_active", "=", true)
.where("email", "like", "%@example.com")
.execute();
// OR conditions
const users = await db
.selectFrom("users")
.selectAll()
.where((eb) =>
eb.or([
eb("name", "=", "John"),
eb("name", "=", "Jane"),
])
)
.execute();
// Kysely automatically infers alias types
const result = await db
.selectFrom("users")
.select([
"id",
"email",
"name as userName", // Alias parsed and typed correctly
])
.executeTakeFirst();
// result.userName is typed correctly
// Inner join
const postsWithAuthors = await db
.selectFrom("posts")
.innerJoin("users", "users.id", "posts.author_id")
.select([
"posts.id",
"posts.title",
"users.name as authorName",
])
.execute();
// Left join
const usersWithPosts = await db
.selectFrom("users")
.leftJoin("posts", "posts.author_id", "users.id")
.select([
"users.id",
"users.name",
"posts.title as postTitle",
])
.execute();
// Subquery in select
const usersWithPostCount = await db
.selectFrom("users")
.select([
"users.id",
"users.name",
(eb) =>
eb
.selectFrom("posts")
.select(eb.fn.count<number>("posts.id").as("count"))
.whereRef("posts.author_id", "=", "users.id")
.as("postCount"),
])
.execute();
// Subquery in where
const usersWithPosts = await db
.selectFrom("users")
.selectAll()
.where("id", "in", (eb) =>
eb.selectFrom("posts").select("author_id").distinct()
)
.execute();
// Single insert
const result = await db
.insertInto("users")
.values({
email: "[email protected]",
name: "John Doe",
is_active: true,
})
.returning(["id", "email", "created_at"])
.executeTakeFirstOrThrow();
// Bulk insert
await db
.insertInto("users")
.values([
{ email: "[email protected]", name: "User 1", is_active: true },
{ email: "[email protected]", name: "User 2", is_active: true },
])
.execute();
// Insert with on conflict (upsert)
await db
.insertInto("users")
.values({
email: "[email protected]",
name: "John",
is_active: true,
})
.onConflict((oc) =>
oc.column("email").doUpdateSet({
name: "John Updated",
updated_at: new Date(),
})
)
.execute();
const result = await db
.updateTable("users")
.set({
name: "Jane Doe",
updated_at: new Date(),
})
.where("id", "=", 1)
.returning(["id", "name", "updated_at"])
.executeTakeFirst();
const result = await db
.deleteFrom("users")
.where("id", "=", 1)
.returning(["id", "email"])
.executeTakeFirst();
await db.transaction().execute(async (trx) => {
const user = await trx
.insertInto("users")
.values({
email: "[email protected]",
name: "User",
is_active: true,
})
.returning(["id"])
.executeTakeFirstOrThrow();
await trx
.insertInto("posts")
.values({
title: "First Post",
author_id: user.id,
})
.execute();
});
Use kysely-codegen to generate types from your existing database:
# Install
npm install -D kysely-codegen
# Generate types (reads from DATABASE_URL environment variable)
npx kysely-codegen
# Specify output file
npx kysely-codegen --out-file src/db/types.ts
Regenerate types whenever the database schema changes.
Transform snake_case column names to camelCase:
import { Kysely, PostgresDialect, CamelCasePlugin } from "kysely";
const db = new Kysely<Database>({
dialect: new PostgresDialect({ pool }),
plugins: [new CamelCasePlugin()],
});
import { KyselyPlugin, PluginTransformQueryArgs, PluginTransformResultArgs } from "kysely";
class LoggingPlugin implements KyselyPlugin {
transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
console.log("Query:", args.node);
return args.node;
}
async transformResult(args: PluginTransformResultArgs): Promise<QueryResult<unknown>> {
console.log("Result:", args.result);
return args.result;
}
}
function findUsers(filters: {
email?: string;
isActive?: boolean;
name?: string;
}) {
let query = db.selectFrom("users").selectAll();
if (filters.email) {
query = query.where("email", "=", filters.email);
}
if (filters.isActive !== undefined) {
query = query.where("is_active", "=", filters.isActive);
}
if (filters.name) {
query = query.where("name", "like", `%${filters.name}%`);
}
return query.execute();
}
import { sql } from "kysely";
// Raw expression in select
const result = await db
.selectFrom("users")
.select([
"id",
sql<string>`CONCAT(first_name, ' ', last_name)`.as("fullName"),
])
.execute();
// Raw expression in where
const users = await db
.selectFrom("users")
.selectAll()
.where(sql`LOWER(email)`, "=", "[email protected]")
.execute();
const result = await db
.with("active_users", (db) =>
db.selectFrom("users").selectAll().where("is_active", "=", true)
)
.selectFrom("active_users")
.selectAll()
.execute();
Enable strict mode in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"target": "ES2020"
}
}
Use TypeScript 5.4 or later for best type inference.
selectAll() when you only need specific fieldsimport { NoResultError } from "kysely";
try {
const user = await db
.selectFrom("users")
.selectAll()
.where("id", "=", 999)
.executeTakeFirstOrThrow();
} catch (error) {
if (error instanceof NoResultError) {
// Handle not found
}
throw error;
}
// Create reusable query parts
function withActiveUsers(db: Kysely<Database>) {
return db.selectFrom("users").where("is_active", "=", true);
}
// Use in queries
const activeUsers = await withActiveUsers(db).selectAll().execute();
Kysely provides a simple migration system:
import { Migrator, FileMigrationProvider } from "kysely";
import path from "path";
import { promises as fs } from "fs";
const migrator = new Migrator({
db,
provider: new FileMigrationProvider({
fs,
path,
migrationFolder: path.join(__dirname, "migrations"),
}),
});
// Run migrations
await migrator.migrateToLatest();
Migration file example:
// migrations/001_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(255)")
.addColumn("is_active", "boolean", (col) => col.defaultTo(true))
.addColumn("created_at", "timestamp", (col) =>
col.defaultTo(sql`now()`).notNull()
)
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable("users").execute();
}
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Set up and use 1Password CLI (op). Use when installing the CLI, enabling desktop app integration, signing in (single or multi-account), or reading/injecting/running secrets via op.
CLI to manage emails via IMAP/SMTP. Use `himalaya` to list, read, write, reply, forward, search, and organize emails from the terminal. Supports multiple accounts and message composition with MML (MIME Meta Language).
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Set up and use 1Password CLI (op). Use when installing the CLI, enabling desktop app integration, signing in (single or multi-account), or reading/injecting/running secrets via op.
CLI to manage emails via IMAP/SMTP. Use `himalaya` to list, read, write, reply, forward, search, and organize emails from the terminal. Supports multiple accounts and message composition with MML (MIME Meta Language).