Knex.js Patterns
Quick Guide: Use Knex.js (v3.x) as a SQL query builder for PostgreSQL, MySQL, SQLite, and MSSQL. Initialize the knex instance once per application (it creates a connection pool internally via tarn.js). Set pool
min: 0so idle connections are released. Always use parameterized bindings (?for values,??for identifiers) inknex.raw()-- never interpolate user input. Wrap multi-table writes inknex.transaction()and always return or await the promise (otherwise the transaction hangs). Use.returning()on PostgreSQL/MSSQL for inserted/updated rows -- it is a no-op on MySQL/SQLite. Callknex.destroy()on graceful shutdown to drain the pool.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST initialize the knex instance ONCE per application and reuse it -- creating multiple instances leaks connection pools)
(You MUST use parameterized bindings (? for values, ?? for identifiers) in ALL knex.raw() calls -- string interpolation causes SQL injection)
(You MUST return or await the promise inside knex.transaction() handlers -- failing to do so causes the transaction connection to hang indefinitely)
(You MUST call knex.destroy() on graceful shutdown -- orphaned pools prevent the Node.js process from exiting)
</critical_requirements>
Examples
- Core Patterns -- Initialization, query builder, insert/update/delete, raw queries, TypeScript integration
- Schema & Migrations -- Schema builder, createTable, alterTable, migrations, seeds
- Transactions & Advanced -- Transactions, batch insert, subqueries, connection pooling, multi-tenancy
Additional resources:
- reference.md -- Query method cheat sheet, column types, pool options, anti-patterns, production checklist
Auto-detection: Knex, knex, knexfile, knex.raw, knex.schema, knex.transaction, knex.migrate, knex.seed, batchInsert, query builder, schema builder, SQL query builder, knex.fn.now, knex.ref, knex.destroy, pg, mysql2, sqlite3, better-sqlite3
When to use:
- Building SQL queries programmatically with a fluent API
- Database schema creation and modification (createTable, alterTable)
- Running and managing database migrations (up/down)
- Seeding development/test databases
- Wrapping multi-step database operations in transactions
- Writing raw SQL with safe parameter binding
- Batch inserting large datasets with chunking
Key patterns covered:
- Knex initialization with connection pool configuration
- Fluent query builder (select, where, join, orderBy, groupBy, having)
- Insert, update, delete with
.returning()for PostgreSQL/MSSQL - Schema builder (createTable, alterTable, column types, indexes, foreign keys)
- Migrations (knex migrate:make, up/down, transaction control)
- Seeds (knex seed:make, seed:run)
- Transactions with async/await and isolation levels
- Raw queries with
?value bindings and??identifier bindings - Subqueries as callbacks or builder instances
- Batch insert with
batchInsert()and chunking - TypeScript table type augmentation
- Connection pool tuning (min, max, acquireTimeout, lifetime)
When NOT to use:
- You need a full ORM with model relationships, lifecycle hooks, and identity maps -- use your ORM solution instead
- You need database-specific features Knex doesn't abstract (e.g., PostgreSQL LISTEN/NOTIFY, MySQL fulltext indexes) -- use
knex.raw()for those - Your project already uses a different query layer or ORM and doesn't need a second one
<philosophy>
Philosophy
Knex is a SQL query builder, not an ORM. The core principle: you write SQL, Knex just makes it safer and more portable.
Core principles:
- One instance, one pool -- Initialize knex once. The instance manages a connection pool (tarn.js). Never create multiple knex instances pointing at the same database.
- Parameterize everything -- Use
?bindings for values and??for identifiers. Never interpolate strings into queries. - Migrations are the source of truth -- Schema changes happen through migrations, not ad-hoc
knex.schemacalls in application code. - Transactions for consistency -- Any operation touching multiple tables or needing atomicity must be wrapped in
knex.transaction(). - Knex is dialect-aware, not dialect-hiding -- Knex normalizes common SQL, but database-specific features (e.g.,
.returning()on PostgreSQL,ON DUPLICATE KEYon MySQL) must be handled per-dialect.
<patterns>
Core Patterns
Pattern 1: Knex Initialization
Initialize once per application. The knex instance manages a connection pool internally. See examples/core.md for full examples.
// Good Example -- Proper initialization with pool tuning
import knex from "knex";
const POOL_MIN = 0;
const POOL_MAX = 10;
const ACQUIRE_TIMEOUT_MS = 30_000;
function createDatabase() {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("DATABASE_URL environment variable is required");
}
return knex({
client: "pg",
connection: connectionString,
pool: { min: POOL_MIN, max: POOL_MAX },
acquireConnectionTimeout: ACQUIRE_TIMEOUT_MS,
});
}
export { createDatabase };
Why good: Single instance, environment variable for connection string, pool min: 0 releases idle connections, named constants
// Bad Example -- Multiple instances, hardcoded config
import knex from "knex";
function getUsers() {
const db = knex({ client: "pg", connection: "postgres://localhost/mydb" });
return db("users").select("*");
// Connection pool leaked -- db.destroy() never called
}
Why bad: Creates a new pool per call (leaks connections), hardcoded connection string, select("*") fetches unnecessary columns
Pattern 2: Query Builder Basics
Fluent API for building SELECT queries. See examples/core.md for joins, groupBy, having.
// Good Example -- Typed query with explicit columns
const ACTIVE_STATUS = "active";
const PAGE_SIZE = 25;
const users = await db<User>("users")
.select("id", "name", "email")
.where("status", ACTIVE_STATUS)
.orderBy("created_at", "desc")
.limit(PAGE_SIZE);
Why good: Explicit column selection, typed result, named constants for status and page size
// Bad Example -- select(*) with string interpolation
const users = await db("users").select("*").whereRaw(`status = '${status}'`); // SQL INJECTION
Why bad: select("*") fetches unnecessary data, string interpolation in whereRaw creates SQL injection vulnerability
Pattern 3: Insert / Update / Delete with Returning
.returning() works on PostgreSQL, MSSQL, CockroachDB, and SQLite 3.35+. MySQL ignores it silently. See examples/core.md.
// Good Example -- Insert with returning (PostgreSQL)
const [inserted] = await db("users")
.insert({ name: "Alice", email: "alice@example.com" })
.returning(["id", "created_at"]);
// Good Example -- Update with returning
const [updated] = await db("users")
.where("id", userId)
.update({ name: newName, updated_at: db.fn.now() })
.returning(["id", "name", "updated_at"]);
Why good: .returning() avoids a separate SELECT, db.fn.now() uses database-native timestamp
// Bad Example -- Forgetting returning() on PostgreSQL
await db("users").insert({ name: "Alice" });
// Returns [] (empty array) on PostgreSQL, not the inserted data
// Developer expects the inserted row but gets a useless number
Why bad: Without .returning(), PostgreSQL insert returns row count (not data), forcing an extra SELECT query