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.
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
| Install | ✓ · 2.4s | 23 packages on disk · 9 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | n/a | could not be bundled for the browser (Node-only code, most likely) |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 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.
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.
- You expect TypeScript to prove that every table, alias, join, migration, and raw expression matches the database. Knex generics cannot check a live schema.
- You want relation loading, nested object writes, entity hooks, or change tracking. The README describes Knex as a query builder and lists separate ORM projects.
- The same query must behave identically on every supported database. `returning`, locks, timeouts, JSON operators, DDL, and upserts remain dialect-specific.
- The target is a browser or edge isolate. Our esbuild browser bundle failed, and normal use depends on Node drivers, sockets, pools, and filesystem migration files.
- A small TypeScript service only needs a typed SQL builder. Knex installed 23 packages and includes CLI, seed, and migration code that Kysely leaves separate.
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
| Package | Registry | Pick it when |
|---|---|---|
| kysely | npm | Use it when stronger TypeScript inference and a SQL-shaped API matter more than bundled CLI and seed tooling. |
| drizzle-orm | npm | Use it for code-defined typed schemas and relation helpers while keeping queries recognizable as SQL. |
| sequelize | npm | Use it when model instances, associations, validation, and lifecycle hooks are desired. |
| typeorm | npm | Use 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.

