mrkeyoor.com_
Tue 22 Sept 22:34 UTC
npmDataupdated 22 Sept 2026

knex review

Knex 3.3.0 is a Node SQL query builder, schema builder, connection pool wrapper, and migration runner. JavaScript chains produce parameterized statements for PostgreSQL, MariaDB, MySQL, CockroachDB, SQL Server, SQLite, Oracle, and Redshift. It deliberately stops short of an ORM: Knex has no entity identity map, relation loader, or automatic model persistence. Version 3.3.0 adds a MariaDB client with `returning`, external pool injection, `migrate.to`, and `migrate.before`, alongside fixes for binding order, SQLite multi-row upserts, schema-qualified locks, streams, and Microsoft SQL Server token credentials.

Verdict

Knex 3.3.0 installed in 2.4 seconds, used 9 MB, passed npm audit with 0 findings, and loaded through both Node module styles. Choose it for SQL-literate Node teams that want mature migrations and wide dialect support, while browser code and projects demanding schema-checked query types should walk away.

We installed it

Lab card: what happened when we installed knexScreenshot of knex documentation
Install✓ · 2.4s23 packages on disk · 9 MB
ImportESM import works · require() works · CommonJS package
Browsern/acould not be bundled for the browser (Node-only code, most likely)
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does knex install cleanly?

Yes. In a fresh container with an empty cache, npm install knex finished in 2 seconds, leaving 23 packages and 9 MB on disk. npm audit reported no known vulnerabilities.

Can knex run in a browser?

Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.

Does knex work with both ESM and CommonJS?

Yes. Both import 'knex' and require('knex') worked in Node 22 in our run. The package is published as CommonJS.

Does knex include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

knex or kysely: which should you use?

kysely: Use it when stronger TypeScript inference and a SQL-shaped API matter more than bundled CLI and seed tooling. Knex 3.3.0 installed in 2.4 seconds, used 9 MB, passed npm audit with 0 findings, and loaded through both Node module styles.

When should you not use knex?

You expect TypeScript to prove that every table, alias, join, migration, and raw expression matches the database. Knex generics cannot check a live schema.

API stability4/5Knex 3.3.0 retains the long-running `knex('table')` query chain, schema builder, transaction callback, pool configuration, and `up` or `down` migration contract. The 3.2.x changelog also records several ESM export fixes and a partial import change that was reverted in 3.2.8. That history makes minor-version testing important for module entry points even though the main query API has stayed recognizable.
Docs5/5The official guide returned HTTP 200 and separates configuration, query methods, raw bindings, schema changes, transactions, streams, migrations, seeds, and extension hooks. Examples show emitted SQL and mark methods by supported dialect. Warnings explain transaction promise hangs, pool behavior, stream dependencies, and timeout limits. The material is large, but the database-specific caveats are stated instead of being hidden behind one generic example.
Maintenance5/5GitHub shows an unarchived repository pushed on June 26, 2026, the same day 3.3.0 shipped, with 20,342 stars and 741 open issues and pull requests. The release includes 6 named features and 9 bug fixes across MariaDB, PostgreSQL, SQLite, SQL Server, streams, types, migrations, and pooling. The queue is large, as expected for an old multi-dialect project, but current releases and cross-database CI are visible.
Ecosystem5/5The npm API counted 5,245,882 downloads in the week ending August 24, 2026. Knex documents drivers for the major Node SQL databases, and its repository maintains an ecosystem list plus recipes. Objection and Bookshelf have used Knex as their lower query layer, so deployment knowledge is widely available. The tradeoff is version compatibility across Knex, the chosen driver, optional `pg-query-stream`, and the database server.

Use it if

  • Your team knows SQL and wants composable, parameterized queries without entity classes or automatic relation loading.
  • One Node service needs query building, pooling, migrations, seeds, transactions, and streaming in the same package.
  • The application supports several SQL engines and accepts explicit branches for features that differ by dialect.
  • Existing code already uses Knex plugins or an ORM such as Objection that builds on Knex's query layer.
Skip it if

Setup reality

We installed Knex 3.3.0 in a fresh Node 22 Bookworm sandbox in 2.4 seconds. It left 23 packages and 9 MB on disk. npm audit reported 0 known vulnerabilities. The package contains 14 direct dependencies, 1 peer dependency, bundled TypeScript declarations, and 1,624 KB unpacked. Both CommonJS require() and ESM import worked on Node 22.23.2. An esbuild browser bundle failed, which is the expected boundary for a Node SQL tool.

Knex does not install your database driver. Add pg, mysql2, mariadb, better-sqlite3, sqlite3, tedious, or oracledb to match the selected client. Native SQLite and Oracle installs can bring platform binaries or build tools into CI. Put the client, connection, pool, and migration directory in application config or a knexfile, while secrets stay in environment variables. SQLite also needs a filename and commonly useNullAsDefault.

Pools require capacity math across every Node process. Set min, max, and acquireConnectionTimeout against the database's connection limit rather than copying a sample. PostgreSQL streaming adds the pg-query-stream peer dependency. In a transaction callback, every query must use trx, and the callback must return or await its work. The transaction guide warns that an unreturned promise can leave the connection hanging. Short scripts and tests should call knex.destroy() so 1 open pool does not keep Node alive.

Migrations use a history table and lock table. Run migrate:latest once under deployment coordination instead of from every replica at boot. Version 3.3.0 adds targeted migrate.to and migrate.before controls, which help staged rollouts but also make the chosen boundary part of release procedure. Test generated SQL on each actual engine: MariaDB returning is new in 3.3.0, while lock modes, timeout cancellation, identifiers, JSON expressions, and schema changes still vary by driver and server.

Patterns

Create a bounded PostgreSQL pool connect-postgres

import knex from 'knex';

export const db = knex({
  client: 'pg',
  connection: process.env.DATABASE_URL,
  pool: { min: 0, max: 10 },
  acquireConnectionTimeout: 10_000,
});

Install `pg` separately. A 10-connection maximum applies per Knex instance, so multiply it by the number of application processes.

Filter and limit a result select-rows

const users = await db('users')
  .select('id', 'email', 'created_at')
  .where({ active: true })
  .where('created_at', '>=', cutoff)
  .orderBy('created_at', 'desc')
  .limit(50);

Knex binds the 2 values in this query. Table and column identifiers are SQL syntax, so do not accept arbitrary identifier strings from a request.

Count joined orders join-and-count

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');

PostgreSQL drivers commonly return a 64-bit count as a string. Decide on parser behavior before treating `order_count` as a JavaScript number.

Insert and return a row insert-returning

const [user] = await db('users')
  .insert({ email: 'ada@example.com', active: true })
  .returning(['id', 'email', 'active']);

Version 3.3.0 adds MariaDB `returning` support. Result shapes still differ by dialect, so test this on the selected driver.

Merge on a unique email upsert-record

await db('users')
  .insert({ email, display_name: displayName })
  .onConflict('email')
  .merge({ display_name: displayName, updated_at: db.fn.now() });

The conflict target needs a matching unique or primary constraint. Supported conflict syntax differs across Knex dialects.

Run one atomic transfer run-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 });
});

All 3 statements use `trx`. A thrown error rolls them back, while returning from the async callback commits them.

Lock an account before updating lock-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` belongs inside a transaction and is not implemented the same way by every supported database.

Create and remove a users table create-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 these 2 functions through the migration runner. Calling `up` directly bypasses Knex's migration history and lock tables.

Stop at a named migration migrate-to-boundary

await db.migrate.to('20260626_add_accounts.js');

`migrate.to` is new in Knex 3.3.0. Make the filename an explicit deployment boundary and verify the database state before the next application version starts.

Bind a raw database expression bind-raw-sql

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. This PostgreSQL expression is not portable to every Knex client.

Inspect SQL without running it inspect-sql

const query = db('users').where({ active: true }).select('id');
const compiled = query.toSQL();
console.log(compiled.sql, compiled.bindings);

`toSQL()` exposes SQL and bindings for tests. Production logs should omit sensitive binding values.

Release a script's connections close-pool

try {
  await runJob(db);
} finally {
  await db.destroy();
}

One unresolved Knex pool can keep a short Node process alive. Servers should instead reuse a shared instance for their lifetime.

Alternatives

PackageRegistryPick it when
kyselynpmUse it when stronger TypeScript inference and a SQL-shaped API matter more than bundled CLI and seed tooling.
drizzle-ormnpmUse it for code-defined typed schemas and relation helpers while keeping queries recognizable as SQL.
sequelizenpmUse it when model instances, associations, validation, and lifecycle hooks are desired.
typeormnpmUse it in decorator-heavy TypeScript or NestJS projects that want entities and repositories.

More data guides

numpy · fsspec · pandas · sqlalchemy · pyarrow · lxml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.