mrkeyoor.com_
Sat 08 Aug 21:00 UTC
npmDataupdated 08 Aug 2026

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.

Verdict

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.

API stability4/5The central knex('table').select/insert/update/delete chain, schema builder, transaction callback, and migration contract have years of continuity. Major releases do remove deprecated behavior and tighten supported Node versions, and dialect-specific methods evolve with database capabilities. Version 3.3.0 requires Node 16 or newer, so upgrades still deserve the repository's dedicated migration guide.
Docs5/5The official guide separates configuration, query methods, raw expressions, schema operations, transactions, streams, migrations, seeds, and extension points. Method headings show dialect support, examples print generated SQL, and warnings cover transaction hangs and cancellation limits. The breadth can be hard to scan, but it is far more honest about cross-database differences than most query builders.
Maintenance5/5Version 3.3.0 was published June 26, 2026 and the repository was pushed the same day. It is not archived, runs a multi-database CI suite, and maintains upgrade documentation. GitHub reports 732 open issues and pull requests, a large queue that reflects the project's age and dialect breadth, but current releases and active work provide strong evidence of ongoing maintenance.
Ecosystem5/5Knex has 20,339 GitHub stars and recorded 4,944,424 downloads in the measured week. It supports major relational databases, has established adapters and hosting recipes, and its ecosystem document lists plugins and higher-level tools. Objection.js, Bookshelf, and other libraries have historically built on its query layer, which makes existing integration knowledge easy to find.

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
Skip it if

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

PackageRegistryPick it when
kyselynpmYou want a lighter SQL-shaped query builder with stronger TypeScript inference and separate migration choices
drizzle-ormnpmYou want code-defined typed schemas and relation helpers while keeping queries close to SQL
sequelizenpmYou want a mature model-based ORM with associations, hooks, validation, and migrations
typeormnpmYou are in a decorator-heavy TypeScript or NestJS codebase that wants entity and repository patterns