Skip to content

What it is

Kysely is a type-safe SQL query builder for TypeScript. It is not an ORM — you write SQL structure, and the types follow your database schema exactly.

Declare an interface describing your tables, then build queries. Every clause narrows the result type, so selecting a column you did not join is a compile error.

Installation

npm install kysely

Getting started

The smallest useful thing you can do with it, and what each part means.

Database interface and a query
interface Database {
  books: { id: Generated<number>; title: string; year: number; author_id: number };
  authors: { id: Generated<number>; name: string };
}

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

const rows = await db
  .selectFrom('books')
  .innerJoin('authors', 'authors.id', 'books.author_id')
  .select(['books.title', 'authors.name as author'])
  .where('books.year', '>=', 1990)
  .orderBy('books.year', 'desc')
  .limit(10)
  .execute();

// rows[0].author is string; rows[0].year is a compile error (not selected)
The aliased column becomes `author` in the result type, and columns you did not select simply do not exist on it — the inference is genuinely precise.
Insert, update and returning
const inserted = await db
  .insertInto('books')
  .values({ title: 'Dune', year: 1965, author_id: 1 })
  .returningAll()
  .executeTakeFirstOrThrow();

await db
  .updateTable('books')
  .set({ year: 1966 })
  .where('id', '=', inserted.id)
  .execute();
executeTakeFirstOrThrow removes the `| undefined` from the type, which is usually what you want after an insert that must have succeeded.

Advanced usage

Where the library earns its place over a simpler alternative.

Transactions and reusable query fragments
await db.transaction().execute(async (trx) => {
  await trx.updateTable('stock')
    .set((eb) => ({ quantity: eb('quantity', '-', qty) }))
    .where('sku', '=', sku)
    .execute();

  await trx.insertInto('orders').values({ sku, qty }).execute();
});

// Compose filters as reusable functions.
const publishedOnly = (qb: SelectQueryBuilder<Database, 'books', {}>) =>
  qb.where('books.published', '=', true);

const results = await publishedOnly(db.selectFrom('books')).selectAll().execute();
The expression builder keeps arithmetic in SQL so it stays atomic. Query builders are values, so shared filters can be extracted like any other function.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Column does not exist on the result type
It was not included in select(). Kysely types the result from exactly what you selected.
Types drift from the real database
The interface is hand-maintained unless generated. Run kysely-codegen in CI and fail on a diff.

Best practices

  • Generate the Database interface from your real schema with kysely-codegen to prevent drift.
  • Use executeTakeFirstOrThrow when exactly one row is expected — it removes the undefined from the type.
  • Choose Kysely when the team knows SQL and wants no abstraction between intent and query.
  • Keep the interface in one file so schema changes surface as compile errors everywhere.

Background

Why it exists, and what it was reacting to.

Kysely exists for developers who want SQL, not an object abstraction, but refuse to give up type safety. Its inference is unusually thorough: joins, aliases and selections all narrow the result type correctly.