Why Raw SQL Queries Are Becoming a Liability

Raw SQL queries have been the backbone of backend development for decades. They are fast, flexible, and universally understood. But as TypeScript has risen to dominate modern web development — powering over 38% of all GitHub pull requests in 2024 — the cracks in raw SQL are becoming impossible to ignore.

The problems are well-documented:

  • No autocompletion: You are writing strings, not code. Your IDE cannot help you.
  • No compile-time checks: A misspelled column name only surfaces at runtime, sometimes in production.
  • SQL injection risk: Even experienced developers occasionally forget to parameterize inputs.
  • Refactoring nightmares: Rename a database column and you must grep through every raw query in your codebase.

For years, the answer was to adopt a full-blown ORM like TypeORM, Sequelize, or Prisma. But ORMs come with their own trade-offs: abstraction leaks, performance overhead, and the dreaded moment when you need a complex query the ORM simply cannot express.

This is where Kysely enters the picture — a query builder that gives you the safety of TypeScript without taking away your SQL.

What Is Kysely?

Kysely (pronounced “Key-seh-lee,” the Finnish word for “query”) is an open-source, type-safe SQL query builder for TypeScript. Created by Sami Koskimäki, it was first released in 2021 and has since grown to over 11,000 stars on GitHub and more than 300,000 weekly npm downloads as of mid-2025.

At its core, Kysely does one thing exceptionally well: it lets you write SQL queries using TypeScript, with end-to-end type safety from your database schema definition all the way to the query result.

It is not an ORM. It does not try to abstract away SQL. Instead, it embraces SQL syntax and wraps it in a fluent, chainable API that your TypeScript compiler can fully understand.

Key Features at a Glance

  • 100% type-safe: Every table, column, join, and return type is checked at compile time.
  • Zero dependencies at its core.
  • SQL-first philosophy: The API mirrors SQL syntax closely, so there is virtually no learning curve for anyone who knows SQL.
  • Dialect-based architecture: First-class support for PostgreSQL, MySQL, and SQLite, with community plugins for many more.
  • Lightweight: The core package is around 50 KB minified.
  • Supports complex queries: Subqueries, CTEs (Common Table Expressions), window functions, JSON operations, and raw SQL escape hatches.

Setting Up Kysely in a TypeScript Project

Getting started with Kysely is straightforward. Let us walk through a PostgreSQL setup.

Step 1: Install Dependencies

npm install kysely pg

For MySQL, you would install mysql2 instead of pg. For SQLite, use better-sqlite3.

Step 2: Define Your Database Schema as TypeScript Interfaces

This is where the magic begins. You declare your database structure using plain TypeScript interfaces:

import { Generated, Insertable, Selectable, Updateable } from 'kysely';

interface Database {
  users: UsersTable;
  posts: PostsTable;
}

interface UsersTable {
  id: Generated<number>;
  email: string;
  first_name: string;
  last_name: string;
  created_at: Generated<Date>;
}

interface PostsTable {
  id: Generated<number>;
  title: string;
  content: string;
  author_id: number;
  published: boolean;
  created_at: Generated<Date>;
}

// Helper types for insert and update operations
type NewUser = Insertable<UsersTable>;
type UserUpdate = Updateable<UsersTable>;
type User = Selectable<UsersTable>;

The Generated type tells Kysely that a column has a default value (like auto-increment or DEFAULT NOW()), so it becomes optional during inserts but present in select results.

Step 3: Create the Kysely Instance

import { Kysely, PostgresDialect } from 'kysely';
import { Pool } from 'pg';

const db = new Kysely<Database>({
  dialect: new PostgresDialect({
    pool: new Pool({
      host: 'localhost',
      database: 'myapp',
      user: 'postgres',
      password: 'secret',
    }),
  }),
});

That is it. You now have a fully type-safe database client.

Writing Queries with Kysely: Practical Examples

Let us see Kysely in action with real-world query patterns.

Simple SELECT with Filtering

const users = await db
  .selectFrom('users')
  .select(['id', 'email', 'first_name'])
  .where('email', 'like', '%@gmail.com')
  .orderBy('created_at', 'desc')
  .limit(10)
  .execute();

// TypeScript knows: users is { id: number; email: string; first_name: string }[]

Notice how the result type is automatically inferred based on the columns you selected. If you try to access users[0].last_name, TypeScript throws a compile-time error because you did not select that column.

INSERT with Returning

const newUser = await db
  .insertInto('users')
  .values({
    email: 'jane@example.com',
    first_name: 'Jane',
    last_name: 'Doe',
  })
  .returningAll()
  .executeTakeFirstOrThrow();

// newUser is fully typed: { id: number; email: string; first_name: string; ... }

JOIN Queries

const postsWithAuthors = await db
  .selectFrom('posts')
  .innerJoin('users', 'users.id', 'posts.author_id')
  .select([
    'posts.id',
    'posts.title',
    'users.first_name as author_first_name',
    'users.email as author_email',
  ])
  .where('posts.published', '=', true)
  .execute();

Kysely checks that the join condition references valid columns on both tables. If you rename author_id to user_id in your schema interface and forget to update this query, the compiler catches it immediately.

Complex Queries: Subqueries and CTEs

const result = await db
  .with('active_authors', (qb) =>
    qb
      .selectFrom('users')
      .select(['id', 'first_name'])
      .where('created_at', '>', new Date('2024-01-01'))
  )
  .selectFrom('active_authors')
  .innerJoin('posts', 'posts.author_id', 'active_authors.id')
  .select(['active_authors.first_name', 'posts.title'])
  .execute();

Even CTEs (Common Table Expressions) are fully typed. The active_authors CTE exposes only id and first_name, and Kysely enforces this in the subsequent query.

Kysely vs. Other Solutions: A Detailed Comparison

How does Kysely stack up against the alternatives? Here is a comparison across the criteria that matter most in production projects:

FeatureRaw SQL (pg/mysql2)PrismaTypeORMKysely
Type-safe queries❌ No✅ Yes⚠️ Partial✅ Yes
Type-safe results❌ No✅ Yes⚠️ Partial✅ Yes
Complex query support✅ Full SQL⚠️ Limited⚠️ Limited✅ Full SQL
Learning curveLow (if you know SQL)MediumHighLow
Bundle size~0 KB~2 MB+ (with engine)~500 KB~50 KB
Migration tools❌ None✅ Built-in✅ Built-in⚠️ Basic / third-party
SQL control✅ Total❌ Abstracted⚠️ Mixed✅ Total
Runtime overheadNoneMediumMediumMinimal

When Kysely Wins

  • Performance-critical applications where you need fine-tuned SQL and cannot afford ORM overhead.
  • Complex reporting queries with CTEs, window functions, or lateral joins that ORMs struggle to express.
  • Teams with strong SQL skills who want safety without abstraction.
  • Microservices and serverless functions where a 2 MB Prisma engine is too heavy.

When an ORM Might Be Better

  • Rapid prototyping where auto-generated migrations and CRUD operations save time.
  • Teams with junior developers who are less comfortable writing SQL.
  • Projects with simple data models that do not require complex queries.

At Lueur Externe, our development team frequently evaluates these trade-offs for our clients. Whether a project calls for the full abstraction of Prisma or the surgical precision of Kysely depends on the specific technical context — and we always recommend the tool that best serves the project’s long-term maintainability.

Advanced Patterns and Best Practices

Schema Generation with kysely-codegen

Manually writing TypeScript interfaces for a large database is tedious and error-prone. The kysely-codegen package introspects your existing database and generates the type definitions automatically:

npx kysely-codegen --url postgres://user:pass@localhost/mydb

This produces a .d.ts file with interfaces that match your actual database schema. Run it as part of your CI pipeline to keep types in sync.

Migrations with Kysely

Kysely includes a lightweight migration system. Migrations are written in TypeScript and are themselves type-checked:

import { Kysely, sql } from 'kysely';

export async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable('comments')
    .addColumn('id', 'serial', (col) => col.primaryKey())
    .addColumn('post_id', 'integer', (col) =>
      col.references('posts.id').onDelete('cascade').notNull()
    )
    .addColumn('body', 'text', (col) => col.notNull())
    .addColumn('created_at', 'timestamp', (col) =>
      col.defaultTo(sql`now()`).notNull()
    )
    .execute();
}

export async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable('comments').execute();
}

Raw SQL Escape Hatch

Sometimes you need database-specific syntax that Kysely does not cover. The sql template tag lets you drop into raw SQL while still maintaining parameterization:

import { sql } from 'kysely';

const result = await db
  .selectFrom('users')
  .select([
    'id',
    'email',
    sql<number>`extract(year from created_at)`.as('signup_year'),
  ])
  .execute();

// result[0].signup_year is typed as number

The generic parameter <number> on the sql tag tells TypeScript what type this expression returns. You retain type safety even when writing raw SQL fragments.

Transaction Support

await db.transaction().execute(async (trx) => {
  const user = await trx
    .insertInto('users')
    .values({ email: 'new@example.com', first_name: 'New', last_name: 'User' })
    .returningAll()
    .executeTakeFirstOrThrow();

  await trx
    .insertInto('posts')
    .values({
      title: 'My First Post',
      content: 'Hello world!',
      author_id: user.id,
      published: false,
    })
    .execute();
});

The transaction callback receives a trx object that has the exact same API as the main db instance, but all queries within it run inside a single database transaction.

Real-World Performance Considerations

Kysely adds virtually zero runtime overhead. Under the hood, it constructs SQL strings and parameter arrays, then passes them directly to the underlying database driver (pg, mysql2, etc.). There is no runtime type checking, no query plan caching layer, no entity hydration — just string building and parameter binding.

Benchmarks from the community show that Kysely queries execute within 1-3% of raw driver performance, which is effectively negligible. Compare this to Prisma, which introduces its own Rust-based query engine that can add 10-50ms of cold start latency in serverless environments.

For serverless and edge computing scenarios — AWS Lambda, Cloudflare Workers, Vercel Edge Functions — Kysely’s minimal footprint is a significant advantage. This is something our team at Lueur Externe has observed firsthand when optimizing serverless architectures on AWS for our clients, where every millisecond of cold start time directly impacts user experience.

Common Pitfalls and How to Avoid Them

  • Forgetting to run kysely-codegen after schema changes: Your types drift from reality. Automate regeneration in your CI/CD pipeline.
  • Overusing the raw SQL escape hatch: If you find yourself writing mostly sql template tags, you are losing the type-safety benefits. Check the Kysely docs — the feature you need might already be supported.
  • Not using .executeTakeFirstOrThrow(): When you expect exactly one result, use this method instead of .execute() followed by manual array indexing. It throws a clear error instead of returning undefined.
  • Ignoring connection pooling: Kysely does not manage connection pools itself — your underlying driver does. Configure pg.Pool or mysql2.createPool properly for your workload.

The Ecosystem Around Kysely

Kysely’s ecosystem is growing rapidly:

  • kysely-codegen: Auto-generate types from your database.
  • kysely-ctl: A CLI tool for running migrations.
  • kysely-planetscale: Dialect for PlanetScale’s serverless MySQL.
  • kysely-d1: Dialect for Cloudflare D1.
  • kysely-bun-sqlite: SQLite dialect for the Bun runtime.
  • kanel-kysely: Generate Kysely types from PostgreSQL using Kanel.

The community-driven nature of these plugins means Kysely is increasingly usable across modern deployment targets.

Should You Adopt Kysely in 2025?

If your project meets any of these criteria, Kysely deserves serious consideration:

  • You are building a TypeScript backend and want compile-time guarantees for your database layer.
  • You are comfortable with SQL and do not want an ORM to abstract it away.
  • You need to write complex queries (analytics, reporting, multi-join aggregations) that ORMs handle poorly.
  • You are deploying to serverless or edge environments where bundle size and cold start matter.
  • You are migrating away from raw SQL queries and want a gradual, low-risk path to type safety.

For simpler CRUD applications or teams that prefer a higher-level abstraction, Prisma remains an excellent choice. The best tool depends on the job.

Conclusion: Type-Safe SQL Is the Future

Kysely represents a maturing trend in the TypeScript ecosystem: developers want type safety everywhere, including the database layer, but they do not want to sacrifice control. Kysely delivers on this promise with a clean, SQL-first API, minimal overhead, and an increasingly robust ecosystem.

The days of unchecked raw SQL strings in TypeScript codebases are numbered. Whether you choose Kysely, Prisma, or Drizzle (another rising contender), investing in type-safe database access pays dividends in fewer bugs, easier refactoring, and faster developer onboarding.

At Lueur Externe, we help businesses build robust, high-performance web applications using the right tools for the job — from TypeScript query builders to AWS-optimized infrastructure. If you are evaluating your database strategy or planning a migration to a type-safe stack, our team of certified experts is here to help.

Get in touch with Lueur Externe → to discuss your project and discover how we can help you ship safer, faster code.