Knex vs Drizzle in 2026: Query Builder vs Type-Safe ORM
·PkgPulse Team
TL;DR
Drizzle for new TypeScript projects; Knex for existing JS codebases or when raw control is needed. Knex (~3M weekly downloads) is a battle-tested JavaScript query builder that's been the standard for decade-old Node.js codebases. Drizzle (~2M downloads) is TypeScript-native, provides schema definition alongside queries, and has better DX. If you're starting fresh in TypeScript, there's no reason to choose Knex.
Key Takeaways
- Knex: ~3M weekly downloads — Drizzle: ~2M (npm, and growing faster)
- Knex is JavaScript-first — TypeScript types added later, less natural
- Drizzle was built TypeScript-first — full inference without configuration
- Knex has migrations built in —
knex migrate:latest - Drizzle has drizzle-kit — schema-based migration generation
Query Comparison
// Knex — JavaScript query builder
const knex = require('knex')({
client: 'postgresql',
connection: process.env.DATABASE_URL,
});
// Select
const users = await knex('users')
.select('id', 'email', 'name')
.where({ role: 'admin' })
.where('created_at', '>', '2026-01-01')
.orderBy('created_at', 'desc')
.limit(10);
// Returns any[] — no type safety unless you add type assertions
// Join
const posts = await knex('posts')
.join('users', 'posts.author_id', 'users.id')
.select('posts.*', 'users.name as author_name')
.where('posts.published', true);
// Insert with returning
const [user] = await knex('users')
.insert({ email: 'alice@example.com', name: 'Alice' })
.returning('*');
// Drizzle — TypeScript-native queries
import { db } from './db';
import { users, posts } from './schema';
import { eq, gt, desc, and } from 'drizzle-orm';
// Select — fully typed
const admins = await db
.select({ id: users.id, email: users.email, name: users.name })
.from(users)
.where(and(eq(users.role, 'admin'), gt(users.createdAt, new Date('2026-01-01'))))
.orderBy(desc(users.createdAt))
.limit(10);
// admins is { id: number; email: string; name: string }[] — inferred
// Join — typed to exactly what you select
const postsWithAuthors = await db
.select({ ...posts, authorName: users.name })
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true));
Migration Workflows
// Knex migrations — write SQL manually
// knex migrate:make add_user_role
// Creates: migrations/20260308_add_user_role.js
exports.up = function(knex) {
return knex.schema.table('users', function(table) {
table.enu('role', ['admin', 'user', 'moderator']).defaultTo('user');
table.index('role');
});
};
exports.down = function(knex) {
return knex.schema.table('users', function(table) {
table.dropColumn('role');
});
};
// Run: knex migrate:latest
// Rollback: knex migrate:rollback
# Drizzle migrations — generated from schema changes
# 1. Update schema.ts (add role column)
# 2. Run:
npx drizzle-kit generate
# Drizzle diffs your schema against current DB state
# Creates SQL migration file automatically
# 3. Apply:
npx drizzle-kit migrate
# Or: db.migrate() in code
# Generated migration:
# ALTER TABLE "users" ADD COLUMN "role" text DEFAULT 'user';
Transaction Support
// Knex transactions
await knex.transaction(async (trx) => {
const [user] = await trx('users').insert({ email, name }).returning('*');
await trx('user_audit').insert({
userId: user.id,
action: 'CREATED',
timestamp: new Date(),
});
// Auto-commit on success, auto-rollback on throw
});
// Drizzle transactions
await db.transaction(async (tx) => {
const [user] = await tx.insert(users).values({ email, name }).returning();
await tx.insert(userAudit).values({
userId: user.id,
action: 'CREATED',
timestamp: new Date(),
});
});
Both are solid for transactions. The API is nearly identical.
Raw SQL
// Knex raw queries
const result = await knex.raw(
'SELECT users.*, count(posts.id)::int as post_count FROM users LEFT JOIN posts ON posts.author_id = users.id GROUP BY users.id HAVING count(posts.id) > ?',
[5]
);
// result.rows — any[]
// Drizzle raw queries
import { sql } from 'drizzle-orm';
const result = await db.execute(sql`
SELECT users.*, count(posts.id)::int as post_count
FROM users
LEFT JOIN posts ON posts.author_id = users.id
GROUP BY users.id
HAVING count(posts.id) > ${5}
`);
When to Choose
Choose Drizzle when:
- New TypeScript project (always the better DX)
- You want schema definition in TypeScript with auto-generated migrations
- Edge runtime support is needed (Cloudflare Workers, Vercel Edge)
- Type safety throughout the data layer is a priority
Choose Knex when:
- Existing JavaScript codebase with Knex migrations already set up
- You need maximum flexibility with raw SQL control
- JavaScript-only project (no TypeScript)
- Team is deeply familiar with Knex's migration patterns
- You need complex join queries where Drizzle's builder feels limiting
Compare Knex and Drizzle package health on PkgPulse.
See the live comparison
View knex vs. drizzle on PkgPulse →