knex
Knex is a Node.js SQL query and schema builder for PostgreSQL, MySQL and MariaDB, SQLite, CockroachDB, Redshift, Oracle, and SQL Server. It turns chained JavaScript calls into parameterized SQL, manages connection pools and transactions, and includes migration and seed tooling. It is not an ORM: there is no identity map, relation loader, model lifecycle, or automatic mapping from application classes to tables, which is precisely why SQL-oriented teams often choose it.
Knex remains the dependable middle ground between raw driver calls and a full ORM, with unusually broad dialect coverage. Choose it when your team owns the SQL tradeoffs; choose Kysely or Drizzle when richer compile-time schema feedback matters more than Knex's mature batteries.
Use it if
- You want composable parameterized SQL without adopting entities, decorators, or an ORM object model
- You support more than one SQL dialect and can stay within their shared feature set
- You want query building, pooling, migrations, seeds, transactions, and streaming from one established package
- Your team understands SQL well enough to inspect generated statements and database execution plans
- You want compile-time schema accuracy: Knex's generic row types help, but table names, joins, aliases, migration drift, and many raw expressions are not checked against the live database
- You want ORM relations, nested writes, identity tracking, or change tracking; the README explicitly positions Knex as a query builder and points ORM users to other projects
- You expect identical behavior across databases: the guide labels returning, timeout cancellation, locks, schemas, upserts, and materialized views by dialect because SQL capabilities differ
- You are building for browsers or edge isolates: Knex is designed around Node database drivers, pools, filesystem migrations, and a sizable dependency tree
- You need a small, SQL-shaped TypeScript API: Kysely is more type-focused, while Knex 3.3.0 includes CLI and migration machinery plus 14 direct runtime dependencies
Setup reality
Installing knex is only the first half. Install the driver for your database too: pg for PostgreSQL, mysql2 for MySQL or MariaDB, sqlite3 or better-sqlite3 for SQLite, tedious for SQL Server, or oracledb for Oracle. Native SQLite and Oracle paths can require platform binaries, Python, compilers, or vendor client libraries; the repository's own setup notes call out Python setuptools and Windows C++ build tools for native dependencies. Put client, connection, pool, and migration settings in a knexfile or application config, then keep credentials in environment variables rather than committing them. Production pools need deliberate min and max sizing, acquireConnectionTimeout, and database-side statement limits. SQLite commonly needs useNullAsDefault and a filename. PostgreSQL streaming additionally needs the pg-query-stream peer package. Migration files have ordered up and down functions and Knex uses a migration lock table, so deployment should run migrate:latest once under controlled coordination, not from every application process. Always return or await the transaction handler's promise; otherwise the docs warn that the connection can hang. Call knex.destroy() in short-lived scripts and tests so the pool does not keep Node alive. Finally, test generated SQL against every supported dialect: returning, lock modes, timeout cancellation, JSON operators, schema changes, and identifier casing are not portable just because the JavaScript chain is accepted.
Patterns
Create a PostgreSQL client and poolconnect-postgres
import knex from 'knex';
export const db = knex({
client: 'pg',
connection: process.env.DATABASE_URL,
pool: { min: 0, max: 10 },
acquireConnectionTimeout: 10000,
});Install pg separately. A pool minimum of 0 avoids stale idle connections in many hosted environments; size the maximum across all processes.
Select filtered rows with ordering and a limitselect-filter-order
const users = await db('users')
.select('id', 'email', 'created_at')
.where({ active: true })
.where('created_at', '>=', cutoff)
.orderBy('created_at', 'desc')
.limit(50);Values are parameterized. Table and column identifiers are not values, so never accept arbitrary user-supplied identifier strings.
Join tables and aggregate resultsjoin-and-aggregate
const rows = await db('users as u')
.leftJoin('orders as o', 'o.user_id', 'u.id')
.select('u.id', 'u.email')
.count({ order_count: 'o.id' })
.groupBy('u.id', 'u.email');Count value types vary by driver and database; PostgreSQL commonly returns bigint counts as strings unless the driver parser is changed.
Insert and return the created rowinsert-returning-row
const [user] = await db('users')
.insert({ email: 'ada@example.com', active: true })
.returning(['id', 'email', 'active']);returning behavior is dialect-specific. PostgreSQL supports it directly; verify result shapes before copying this across databases.
Insert or merge on a conflictupsert-record
await db('users')
.insert({ email, display_name: displayName })
.onConflict('email')
.merge({ display_name: displayName, updated_at: db.fn.now() });The conflict column must have a matching primary or unique constraint, and supported syntax differs among dialects.
Transfer data inside a transactionrun-transaction
await db.transaction(async (trx) => {
await trx('accounts').where({ id: fromId }).decrement('balance', amount);
await trx('accounts').where({ id: toId }).increment('balance', amount);
await trx('transfers').insert({ from_id: fromId, to_id: toId, amount });
});Use trx for every query in the unit of work and await the handler. Throwing rolls back; successful resolution commits.
Lock a row during a transactionlock-row
await db.transaction(async (trx) => {
const account = await trx('accounts')
.where({ id: accountId })
.forUpdate()
.first();
if (!account || account.balance < amount) throw new Error('insufficient funds');
await trx('accounts').where({ id: accountId }).decrement('balance', amount);
});forUpdate is meaningful only inside a transaction and is not supported by every Knex dialect.
Define an idempotent schema migration paircreate-migration
export async function up(knex) {
await knex.schema.createTable('users', (table) => {
table.bigIncrements('id').primary();
table.string('email').notNullable().unique();
table.timestamps(true, true);
});
}
export async function down(knex) {
await knex.schema.dropTable('users');
}Run migrations through the Knex migration runner so its lock and history tables stay authoritative; do not call up directly.
Add a safely bound raw expressionuse-raw-fragment
const rows = await db('events')
.select('id')
.select(db.raw('date_trunc(?, ??) as bucket', ['day', 'created_at']))
.whereRaw('payload ->> ? = ?', ['type', 'purchase']);Use ? for values and ?? for identifiers. Raw SQL reduces portability, so isolate it and test against the target database.
Inspect SQL without executing itinspect-generated-sql
const query = db('users').where({ active: true }).select('id');
const compiled = query.toSQL();
console.log(compiled.sql, compiled.bindings);toSQL is useful in tests and debugging. Avoid logging sensitive bindings in production.
Consume a large result as a streamstream-large-query
const stream = db('events').select('*').orderBy('id').stream();
try {
for await (const row of stream) {
await processRow(row);
}
} finally {
stream.destroy();
}PostgreSQL streaming requires pg-query-stream. Destroy the stream if an HTTP client disconnects or iteration stops early.
Close connections in a scriptclose-pool
import { db } from './database.js';
try {
await runJob(db);
} finally {
await db.destroy();
}Long-running servers keep one shared Knex instance. Short-lived scripts and test suites should destroy it or the pool may keep Node running.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| kysely | npm | You want a lighter SQL-shaped query builder with stronger TypeScript inference and separate migration choices |
| drizzle-orm | npm | You want code-defined typed schemas and relation helpers while keeping queries close to SQL |
| sequelize | npm | You want a mature model-based ORM with associations, hooks, validation, and migrations |
| typeorm | npm | You are in a decorator-heavy TypeScript or NestJS codebase that wants entity and repository patterns |