Skip to main content

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 inknex 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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.