← Back to articles

Kysely vs Knex vs Raw SQL: Best Query Builder for Node.js (2026)

Not every project needs an ORM. Sometimes you want SQL control without the abstraction overhead. Query builders sit in the sweet spot: SQL-like API with programmatic composition. Kysely (type-safe), Knex (battle-tested), and raw SQL (direct) represent three approaches.

Quick Comparison

FeatureKyselyKnex.jsRaw SQL
Type safetyFull (TypeScript)None (JavaScript)None
Query compositionProgrammaticProgrammaticString concatenation
MigrationsYesYes (mature)Manual
Bundle size~20KB~45KB0
Learning curveLow-mediumLowLowest (know SQL)
Multi-dialectPostgreSQL, MySQL, SQLite7+ databasesDatabase-specific
PerformanceNear-rawNear-rawFastest
ParameterizationAutomaticAutomaticManual

Kysely: Type-Safe Query Builder

Kysely is a type-safe SQL query builder for TypeScript. Define your schema as types, and every query is validated at compile time.

Strengths

Full type safety. Table names, column names, join conditions, where clauses — everything is typed. Typos caught at compile time, not runtime.

interface Database {
  users: { id: number; name: string; email: string; created_at: Date };
  posts: { id: number; user_id: number; title: string; body: string };
}

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

// TypeScript catches errors here
const users = await db
  .selectFrom('users')
  .innerJoin('posts', 'posts.user_id', 'users.id')
  .select(['users.name', 'posts.title'])
  .where('users.email', '=', 'user@example.com')
  .execute();
// users is typed as { name: string; title: string }[]

Autocompletion. Your IDE suggests valid table names, column names, and operators. Massive productivity boost.

SQL-native. Kysely generates SQL that looks like what you'd write by hand. No magic, no hidden queries.

Composable. Build queries programmatically — conditionally add where clauses, joins, and selects without string manipulation.

Migrations. Built-in migration system. Type-safe migration files.

Weaknesses

  • TypeScript only. No JavaScript support (by design).
  • Fewer database dialects than Knex (PostgreSQL, MySQL, SQLite, MSSQL).
  • Schema definition is manual. You maintain TypeScript interfaces for your database schema (or use codegen tools like kysely-codegen).
  • Smaller community than Knex.
  • No seeding built-in.

Best For

TypeScript projects that want SQL control with compile-time safety. Teams who've been burned by runtime SQL errors.

Knex.js: The Battle-Tested Standard

Knex has been the go-to Node.js query builder since 2013. It's mature, widely used, and supports the most databases.

Strengths

Maturity. 10+ years of production use. Every edge case documented, every database quirk handled.

Database support. PostgreSQL, MySQL, MariaDB, SQLite3, Oracle, Amazon Redshift, MSSQL — the widest support of any query builder.

Migration system. Mature, well-tested migrations with rollback support, timestamps, and directory organization.

Seeding. Built-in seed files for populating databases with test/sample data.

Community. Large community, extensive Stack Overflow coverage, and many tutorials.

const users = await knex('users')
  .join('posts', 'users.id', 'posts.user_id')
  .select('users.name', 'posts.title')
  .where('users.email', 'user@example.com');

Weaknesses

  • No type safety. Column name typos, wrong table names — all runtime errors.
  • JavaScript-first. TypeScript support exists but isn't first-class. Types are generic, not schema-aware.
  • Aging codebase. Some APIs feel dated compared to modern alternatives.
  • Callback patterns in older code (though promises are supported).
  • No autocompletion for table/column names.

Best For

JavaScript projects, teams comfortable without type safety, or projects needing broad database support.

Raw SQL: Direct Database Access

Write SQL strings directly using your database driver (pg, mysql2, better-sqlite3).

Strengths

Maximum performance. No query builder overhead. Your SQL runs exactly as written.

No abstraction leaks. Full access to database-specific features: CTEs, window functions, JSON operators, recursive queries — no checking if the query builder supports it.

Zero dependencies. Just a database driver. Smallest possible footprint.

Portability of knowledge. SQL skills transfer everywhere — not tied to any library's API.

const { rows } = await pool.query(
  `SELECT u.name, p.title
   FROM users u
   JOIN posts p ON p.user_id = u.id
   WHERE u.email = $1`,
  ['user@example.com']
);

Weaknesses

  • No type safety (unless you add sql-tagged-templates or similar).
  • SQL injection risk if you forget parameterization.
  • String manipulation for dynamic queries is error-prone and ugly.
  • No migrations built-in. Use a separate tool (dbmate, golang-migrate).
  • Verbose for complex dynamic queries with optional conditions.

Best For

Simple projects, performance-critical queries, database-specific features, or developers who prefer raw SQL.

Dynamic Query Composition

This is where query builders shine vs. raw SQL:

// Kysely: clean and type-safe
let query = db.selectFrom('users').selectAll();
if (search) query = query.where('name', 'like', `%${search}%`);
if (role) query = query.where('role', '=', role);
if (sortBy) query = query.orderBy(sortBy, order);
const users = await query.limit(20).execute();

// Raw SQL: string manipulation
let sql = 'SELECT * FROM users WHERE 1=1';
const params = [];
if (search) { sql += ' AND name LIKE $' + (params.length + 1); params.push(`%${search}%`); }
if (role) { sql += ' AND role = $' + (params.length + 1); params.push(role); }
if (sortBy) { sql += ` ORDER BY ${sortBy} ${order}`; } // SQL injection risk!
sql += ' LIMIT 20';
const { rows } = await pool.query(sql, params);

For dynamic queries with multiple optional conditions, query builders are significantly safer and more maintainable.

Performance

All three approaches generate SQL that your database executes. The performance differences are in query construction:

ApproachQuery Build TimeQuery ExecutionTotal
Raw SQL~01-50ms1-50ms
Kysely<0.1ms1-50ms1-50ms
Knex<0.2ms1-50ms1-50ms

Query construction overhead is negligible. Database execution time dominates. Choose based on DX, not performance.

Migration Tools

FeatureKyselyKnexRaw SQL Options
Built-in migrationsYesYesNo
RollbackYesYesVaries
TypeScriptYesPluginN/A
Popular alternativesdbmate, golang-migrate, Flyway

For raw SQL projects, dbmate is the recommended migration tool — database-agnostic, SQL-based migrations, simple CLI.

FAQ

Should I use a query builder or an ORM?

Query builders if you: like SQL, want control, have complex queries, or want lightweight. ORMs (Prisma, Drizzle) if you: want schema-as-code, prefer abstraction, or do mostly CRUD.

Can I use Kysely with Prisma?

Yes. Use Prisma for migrations and schema management, Kysely for queries. prisma-kysely generates Kysely types from your Prisma schema. Best of both worlds.

Is Knex still maintained?

Yes, though the pace of development has slowed. It's stable and doesn't need frequent updates — query building is a solved problem. Bug fixes and security updates continue.

When should I use raw SQL?

When your queries are simple (no dynamic conditions), when you need database-specific features, or when you're building a quick script/prototype.

The Verdict

  • Kysely for TypeScript projects. Compile-time safety eliminates an entire class of bugs. Modern DX with autocompletion.
  • Knex for JavaScript projects or when you need broad database support. Battle-tested and reliable.
  • Raw SQL for simple projects, scripts, or when you need maximum database feature access.

For new TypeScript projects in 2026, Kysely is the clear winner. The type safety and autocompletion alone justify the switch from Knex or raw SQL.

Get AI tool guides in your inbox

Weekly deep-dives on the best AI coding tools, automation platforms, and productivity software.