From api-database-knex
SQL query builder for PostgreSQL, MySQL, SQLite, and MSSQL -- fluent queries, schema builder, migrations, seeds, transactions, raw queries
How this skill is triggered — by the user, by Claude, or both
Slash command
/api-database-knex:api-database-knexThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> **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: 0` so idle connections are released. Always use **parameterized bindings** (`?` for values, `??` for identifiers) in `knex.raw()` -- never interpolate user input. Wrap mu...
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>
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>
Additional resources:
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:
Key patterns covered:
.returning() for PostgreSQL/MSSQL? value bindings and ?? identifier bindingsbatchInsert() and chunkingWhen NOT to use:
knex.raw() for thoseKnex is a SQL query builder, not an ORM. The core principle: you write SQL, Knex just makes it safer and more portable.
Core principles:
? bindings for values and ?? for identifiers. Never interpolate strings into queries.knex.schema calls in application code.knex.transaction()..returning() on PostgreSQL, ON DUPLICATE KEY on MySQL) must be handled per-dialect.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
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
.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: "[email protected]" })
.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
Use ? for value bindings and ?? for identifier bindings. See examples/core.md.
// Good Example -- Parameterized raw query
const MIN_ORDER_COUNT = 5;
const results = await db.raw(
`SELECT ??, COUNT(*) as order_count
FROM ??
WHERE ?? > ?
GROUP BY ??
HAVING COUNT(*) >= ?`,
[
"users.id",
"orders",
"orders.created_at",
cutoffDate,
"users.id",
MIN_ORDER_COUNT,
],
);
Why good: ?? for identifiers, ? for values, all user input parameterized
// Bad Example -- String concatenation in raw query
const results = await db.raw(`SELECT * FROM users WHERE name = '${name}'`);
// SQL INJECTION: name = "'; DROP TABLE users; --"
Why bad: String interpolation allows SQL injection, attacker can execute arbitrary SQL
Wrap multi-step operations in transactions. Return or await the promise -- otherwise the connection hangs. See examples/transactions-advanced.md.
// Good Example -- Async/await transaction
const result = await db.transaction(async (trx) => {
const [order] = await trx("orders")
.insert({ user_id: userId, total: amount })
.returning("id");
await trx("order_items").insert(
items.map((item) => ({ order_id: order.id, ...item })),
);
await trx("inventory")
.whereIn(
"product_id",
items.map((i) => i.product_id),
)
.decrement("quantity", 1);
return order;
});
// Transaction auto-commits on success, auto-rolls-back on thrown error
Why good: All operations atomic, auto-commit on success, auto-rollback on error, returns value from transaction
// Bad Example -- Forgetting to return/await inside transaction
await db.transaction((trx) => {
trx("orders").insert({ user_id: userId }); // NOT returned/awaited
trx("items").insert({ order_id: 1 }); // NOT returned/awaited
// Transaction handler returns undefined -- trx NEVER commits or rolls back
// Connection hangs until acquireConnectionTimeout fires
});
Why bad: Without returning a promise, Knex cannot detect completion, transaction hangs indefinitely consuming a pool connection
Create and modify tables. Use in migrations, not application code. See examples/schema-migrations.md.
// Good Example -- Migration creating a table
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable("orders", (table) => {
table.increments("id").primary();
table
.integer("user_id")
.unsigned()
.notNullable()
.references("id")
.inTable("users")
.onDelete("CASCADE");
table.decimal("total", 10, 2).notNullable();
table
.enum("status", ["pending", "paid", "shipped", "cancelled"])
.notNullable()
.defaultTo("pending");
table.timestamps(true, true); // created_at, updated_at with defaults
table.index(["user_id", "status"]);
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTable("orders");
}
Why good: Foreign key with cascade, composite index, enum constraint, timestamps with defaults, reversible down migration
<decision_framework>
What kind of database operation?
-- SELECT query -> db("table").select().where()
-- INSERT -> db("table").insert(data).returning()
-- UPDATE -> db("table").where().update(data).returning()
-- DELETE -> db("table").where().del()
-- Schema change -> db.schema.createTable() / .alterTable() (in migrations only)
-- Complex SQL -> db.raw("SQL", bindings)
-- Batch insert -> db.batchInsert("table", rows, chunkSize)
-- Multi-table atomic write -> db.transaction(async (trx) => { ... })
Can the query builder express this?
-- YES -> Use the query builder (portable, type-safe)
-- NO -> Does it use database-specific syntax?
-- YES -> Use db.raw() with parameterized bindings
-- NO -> Is it a performance-critical query needing exact SQL?
-- YES -> Use db.raw() with parameterized bindings
-- NO -> File an issue or use a subquery callback
Does this operation modify multiple tables?
-- YES -> Use db.transaction()
Does this read need snapshot isolation?
-- YES -> Use db.transaction({ isolationLevel: "repeatable read" })
Is this a single INSERT/UPDATE/DELETE?
-- YES -> No transaction needed (single statement is atomic)
Which database are you targeting?
-- PostgreSQL -> .returning() works, returns array of objects
-- MSSQL -> .returning() works, returns array of objects
-- SQLite 3.35+ -> .returning() works
-- MySQL -> .returning() is silently ignored, insert returns [insertId]
-- Oracle -> .returning() works
</decision_framework>
<red_flags>
High Priority Issues:
knex.raw() or .whereRaw() -- SQL injection vulnerability; always use ? / ?? bindingsknex.transaction() handler -- transaction connection hangs indefinitelyknex.destroy() on shutdown -- orphaned pool prevents process exit, connections leakknex.schema calls in application code instead of migrations -- schema state becomes unpredictable across environmentsMedium Priority Issues:
select("*") in production queries -- fetches unnecessary data, increases memory usage, breaks when columns are added.returning() on PostgreSQL inserts -- returns empty array [] instead of inserted datamin: 0 -- default min: 2 keeps stale connections alive during low-traffic periodsWHERE clause on .update() or .del() -- updates/deletes ALL rows in the tableKEYS-style patterns without pagination -- db("table").select() with no limit loads entire table into memoryCommon Mistakes:
.returning() to work on MySQL -- it is silently ignored; use insertId from the result instead.timeout() on the query without { cancel: true } -- times out the Node.js side but the query keeps running on the database serverdisableTransactions: true and assuming rollback works -- without a transaction, a failed migration leaves the database in a partial stateknex.schema.hasTable() and knex.schema.createTable() are atomic -- another process can create the table between the check and the createtrx.commit() or trx.rollback() AND returning a promise -- double-completion causes unpredictable behaviorGotchas & Edge Cases:
knex.raw() returns a { rows, fields } object on PostgreSQL but a flat array on MySQL -- access .rows for PostgreSQL or destructure accordingly.timestamps(true, true) creates created_at and updated_at with defaultTo(knex.fn.now()) -- but updated_at is NOT automatically updated on row changes; you must set it yourself in UPDATE queries or use a database trigger.first() returns undefined (not null) when no row matches -- check with if (!result) not if (result === null)knex.batchInsert() wraps all chunks in a single transaction by default -- if one chunk fails, all previous chunks are rolled back.returning() must match the database column names exactly (case-sensitive on PostgreSQL).whereIn("id", []) with an empty array generates WHERE 1 = 0 (always false) -- Knex handles it but it can be surprising in logsknex.fn.now() is evaluated by the database server, not Node.js -- useful for consistency but means you can't mock it in tests without stubbing the query</red_flags>
<critical_reminders>
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)
Failure to follow these rules will cause SQL injection vulnerabilities, connection pool exhaustion, hanging transactions, and zombie processes.
</critical_reminders>
Searches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.
npx claudepluginhub agents-inc/skills --plugin api-database-knex