JavaScript developers face a critical choice when working with databases: which query builder to use? While many options exist, Kysely and Knex.js stand out as two powerful contenders with distinct approaches.
Knex.js has been the trusted workhorse since 2012. It gives you a flexible way to build SQL queries in JavaScript while working with almost any database out there. Thousands of projects rely on it daily.
Kysely, the TypeScript-focused newcomer from 2021, takes a different approach. It catches database errors during development through comprehensive type checking, helping you avoid those painful 3 AM production issues.
Let's dive into what makes each tool unique and which one might be the perfect fit for your next project.
What is Knex.js?
Knex.js transforms the way you work with databases in JavaScript. Created by Tim Griesser, this battle-tested query builder lets you craft SQL queries using chainable methods that feel natural to JavaScript developers.
Unlike heavyweight ORMs that hide SQL completely, Knex stays close enough to give you control while eliminating repetitive SQL string building. You'll appreciate its built-in migration system, connection pooling, and transaction management when building serious applications.
Developers choose Knex when they need more fine-grained control than full ORMs provide but don't want to write and maintain raw SQL strings throughout their codebase.
What is Kysely?
Kysely brings the power of TypeScript to database queries. Sami Koskimäki created this innovative tool to bridge a critical gap: catching database errors during development rather than at runtime.
The magic of Kysely lies in its ability to validate your queries as you type them. Your editor becomes a database expert, suggesting table and column names while warning about type mismatches before your code even runs.
Remarkably, Kysely achieves this without feeling alien to Knex users. The API remains familiar and fluent while adding powerful type safety under the hood. This combination makes it particularly attractive for teams building complex applications where reliability is crucial.
Kysely vs. Knex.js: a quick comparison
Before diving deeper, here's how these query builders stack up against each other:
Feature | Kysely | Knex.js |
---|---|---|
TypeScript support | Built for TypeScript with full editor help | Basic TypeScript support added later |
Learning curve | Steeper if you're new to TypeScript | Easier to learn with lots of examples |
Query building | Checks your queries while you type | Flexible queries that check at runtime |
Migration support | Limited built-in tools | Great migration system included |
Performance | Fast with minimal overhead | Proven reliable performance |
Plugin ecosystem | Smaller but growing | Large with many plugins and tools |
Raw SQL support | Type-safe raw queries | Flexible raw query options |
Database support | PostgreSQL, MySQL, SQLite | Wide support including SQL Server, Oracle |
Community size | Smaller but growing quickly | Large community with many resources |
Transaction handling | Type-safe transactions | Flexible transaction support |
Installation and setup
Your journey with either tool begins with installation and configuration. This initial setup reveals much about their philosophies.
Knex.js keeps things simple and familiar. Just install Knex and your database driver:
npm install knex pg # Knex + PostgreSQL driver
A basic Knex setup might look like this:
// knexfile.js
module.exports = {
development: {
client: 'postgresql',
connection: {
database: 'my_db',
user: 'username',
password: 'password'
},
migrations: {
tableName: 'knex_migrations'
}
}
};
// Using it in your app
const knex = require('knex')(require('./knexfile').development);
Kysely takes a more TypeScript-centric approach. After installing the packages:
npm install kysely pg # Kysely + PostgreSQL driver
Your setup defines both database connection and structure:
// Database schema as TypeScript types
interface Database {
users: {
id: number;
username: string;
email: string;
};
posts: {
id: number;
title: string;
user_id: number;
};
}
// Connection setup
import { Kysely, PostgresDialect } from 'kysely';
import { Pool } from 'pg';
const db = new Kysely<Database>({
dialect: new PostgresDialect({
pool: new Pool({ database: 'my_db' })
})
});
While Kysely's setup requires more code upfront, this investment pays dividends through enhanced editor support and compile-time error checking throughout your project.
Query building
Query creation forms the heart of daily interaction with your database tool. The approaches here reveal fundamental differences in philosophy.
Knex.js creates queries through chained methods that closely mirror SQL structure:
// Find recent active users
const users = await knex('users')
.select('id', 'username')
.where('active', true)
.orderBy('created_at', 'desc')
.limit(5);
// Count posts by category
const counts = await knex('posts')
.select('category')
.count('id as total')
.groupBy('category');
Dynamic queries are particularly elegant with Knex:
function findUsers(filters) {
let query = knex('users').select('*');
if (filters.role) {
query = query.where('role', filters.role);
}
if (filters.search) {
query = query.where('username', 'like', `%${filters.search}%`);
}
return query;
}
Kysely builds on this pattern while adding comprehensive type safety:
// Find recent active users
const users = await db
.selectFrom('users')
.select(['id', 'username'])
.where('active', '=', true)
.orderBy('created_at', 'desc')
.limit(5)
.execute();
// Count posts by category
const counts = await db
.selectFrom('posts')
.select('category')
.select(eb => [eb.fn.count('id').as('total')])
.groupBy('category')
.execute();
The magic happens when you make a mistake - your editor immediately highlights errors in table names, column references, or data types before your code runs.
Transaction management
Transactions ensure related database operations succeed or fail together. The approaches here reveal different priorities.
Knex.js offers flexible transaction patterns:
// Transfer funds between accounts
async function transferFunds(from, to, amount) {
return knex.transaction(async trx => {
await trx('accounts').where('id', from).decrement('balance', amount);
await trx('accounts').where('id', to).increment('balance', amount);
await trx('transfers').insert({ from, to, amount });
});
// Auto-commits on success, rolls back on error
}
Kysely creates transactions with automatic safety nets:
// Transfer funds between accounts
async function transferFunds(from: number, to: number, amount: number) {
return db.transaction().execute(async (trx) => {
await trx
.updateTable('accounts')
.where('id', '=', from)
.set({ balance: eb => eb('balance', '-', amount) })
.execute();
await trx
.updateTable('accounts')
.where('id', '=', to)
.set({ balance: eb => eb('balance', '+', amount) })
.execute();
await trx
.insertInto('transfers')
.values({ from, to, amount })
.execute();
});
// Auto-commits on success, rolls back on error
}
The big difference? Kysely's approach ensures type safety throughout the transaction, catching errors early.
Migration support
Projects don't just use databases - they evolve them carefully over time. This capability reveals significant differences between our contenders.
Knex.js shines with its comprehensive migration system:
// migrations/20230501_create_users.js
exports.up = function(knex) {
return knex.schema.createTable('users', table => {
table.increments('id').primary();
table.string('username', 50).unique();
table.boolean('active').defaultTo(true);
table.timestamp('created_at').defaultTo(knex.fn.now());
});
};
exports.down = function(knex) {
return knex.schema.dropTable('users');
};
Running migrations becomes remarkably simple:
npx knex migrate:latest # Apply pending migrations
npx knex migrate:rollback # Undo last batch
Kysely takes a different approach, providing building blocks rather than a complete system:
// migrations/001_create_users.ts
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('users')
.addColumn('id', 'serial', col => col.primaryKey())
.addColumn('username', 'varchar(50)', col => col.unique())
.addColumn('active', 'boolean', col => col.defaultTo(true))
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('users').execute();
}
You'll need your own runner or a third-party tool to manage these migrations, reflecting Kysely's philosophy of providing type-safe components rather than a monolithic system.
Raw SQL support
Sometimes, you need raw SQL power. Both tools offer escape hatches, but with different safety measures.
Knex.js makes raw SQL straightforward:
// Complex join with raw SQL
const stats = await knex.raw(`
SELECT u.username, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.username
HAVING COUNT(p.id) > ?
`, [5]);
Kysely maintains type safety even with raw SQL:
// Complex join with type-safe raw SQL
const stats = await db
.selectFrom(
sql<{ username: string, post_count: number }>`
SELECT u.username, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.username
HAVING COUNT(p.id) > ${5}
`.as('stats')
)
.selectAll()
.execute();
Notice how Kysely's template literals both prevent SQL injection and preserve result typing - a powerful combination for safe raw SQL usage.
Debugging and testing
When things go wrong, visibility and testability become crucial. The approaches here reveal different development priorities.
Knex.js provides simple debugging tools:
// See what SQL will be generated
const query = knex('users').where('active', true);
console.log(query.toString());
// "SELECT * FROM "users" WHERE "active" = true"
Testing with Knex typically uses in-memory SQLite:
// Quick test setup
const knex = require('knex')({
client: 'sqlite3',
connection: { filename: ':memory:' },
useNullAsDefault: true
});
// Test your queries
test('finds active users', async () => {
await knex.schema.createTable('users', t => {
t.increments(); t.boolean('active');
});
await knex('users').insert([{active: true}, {active: false}]);
const users = await knex('users').where('active', true);
expect(users.length).toBe(1);
});
Kysely adds type checking to the debugging process:
// Type-safe query inspection
const query = db.selectFrom('users').where('active', '=', true);
const { sql, parameters } = query.compile();
console.log(sql, parameters);
// "SELECT * FROM "users" WHERE "active" = $1" [true]
Testing with Kysely maintains type safety throughout:
// Type-safe test setup
const db = new Kysely<TestDB>({
dialect: new SqliteDialect({ database: new SQLite(':memory:') })
});
// Your typed test
test('finds active users', async () => {
await db.schema
.createTable('users')
.addColumn('id', 'integer', c => c.primaryKey())
.addColumn('active', 'boolean')
.execute();
await db.insertInto('users').values([
{id: 1, active: true},
{id: 2, active: false}
]).execute();
const users = await db.selectFrom('users')
.where('active', '=', true)
.selectAll()
.execute();
expect(users.length).toBe(1);
});
The key difference? Kysely tests catch type errors at compile time, while Knex tests may fail at runtime.
Final thoughts
This guide showed how Knex.js and Kysely take different approaches to building database queries. Knex.js offers flexibility, wide database support, and a mature ecosystem, making it a great fit for many JavaScript and mixed-code projects.
Kysely, with its strong TypeScript support and type-safe queries, is ideal for teams who want more safety and structure in their code.
Choosing between them depends on your priorities—whether you value flexibility and simplicity or type safety and developer tooling.
Both tools are capable and can serve as a strong foundation for your database layer
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github