Skip to content

Query Builder vs ORM in Node.js: When to Use Each

Purpose

When I design a Node.js application’s data layer, I face a fundamental choice: query builder or ORM? Each has clear trade-offs. I need to understand when to use each approach.

The Direct Answer

Use a query builder (Kysely, Drizzle, Knex) when you need SQL flexibility, complex queries, performance control, or work with existing schemas.

Use a full ORM (Prisma, TypeORM, Mikro-ORM) when you want rapid development, automatic migrations, relationship management, and higher abstraction for CRUD.

What Reddit Developers Say

The discussion showed clear preferences:

Query Builder Advocates:

u/Insensibilities (score 15): “Drizzle: no runtime engine, it is just a thin wrapper on SQL. I also liked Kysely and I find Drizzle similar to it in the sense of great type safety.”

u/Narrow_Relative2149 (score 2): “Problem with ORMs is complexity and compromises so you can migrate from Postgres to another DB and it just does NOT happen. You start with SQL to do something and then spend hours wondering how to write it in some stupid query builder API.”

u/romeeres (score 2): “In Prisma you have limited interface that won’t be able to do lots of SQL features, if you need something extra you’d need to rewrite it to raw SQL.”

ORM Advocates:

u/EvilPencil (score 2): “I like ORMs for the DX on CRUDish operations, not the idea of switching dialects. In six years as primary dev at a SaaS company, switching databases has never been a topic of conversation.”

Key Pattern: Developers who value SQL control prefer query builders. Those prioritizing CRUD DX prefer ORMs. The database-switching argument is theoretical.

The Fundamental Trade-off

Query builders and ORMs represent two ends of a spectrum:

AspectQuery BuilderORM
Abstraction LevelLow (SQL-close)High (domain model)
SQL Knowledge RequiredEssentialHelpful
FlexibilityMaximumLimited by features
BoilerplateMore explicitLess for simple ops
Relationship ManagementManual joinsAuto-loaded relations
Performance ControlFull visibilityAbstracted
Learning CurveSQL transfersORM-specific

What is a Query Builder?

A query builder provides programmatic SQL construction with type safety.

Characteristics:

  • Thin abstraction over raw SQL
  • Full SQL feature access (CTEs, window functions, subqueries)
  • TypeScript type inference for results
  • No hidden queries or N+1 problems
  • Direct control over performance

Popular Query Builders in 2026:

ToolKey FeaturesBest For
KyselyFull type inference, SQL-like APITypeScript-first
DrizzleQuery builder + light ORM, zero depsServerless, SQL experts
KnexMature, migration systemExisting projects

What is a Full ORM?

An ORM maps database tables to objects, managing relationships and persistence.

Characteristics:

  • Domain model abstraction
  • Automatic relationship loading
  • Migration management
  • Unit of Work pattern (some)
  • Schema introspection

Popular ORMs in 2026:

ToolPatternBest For
PrismaSchema-first, Client APIRapid development
TypeORMActive Record / Data MapperEnterprise flexibility
Mikro-ORMData Mapper, Unit of WorkDDD, complex domains

When to Use a Query Builder

1. Complex SQL needed

CTEs, window functions, complex joins, database-specific features.

kysely-window.ts
import { Kysely, sql } from 'kysely';
const result = await db
.selectFrom('users')
.select([
'id',
'name',
sql<number>`row_number() over (order by score desc)`.as('rank')
])
.execute();

2. SQL expertise available

Team knows SQL well, wants direct control, needs to optimize manually.

3. Performance critical

Need to understand every query, avoid hidden ORM overhead, serverless/edge deployments.

4. Existing schema

Legacy database, complex schema not designed for ORM conventions.

5. Type safety without abstraction

TypeScript-first, type inference for results, no code generation step.

Reddit insight: “You start with SQL to do something and then spend hours wondering how to write it in some stupid query builder API” - This frustration is eliminated with query builders since they map directly to SQL.

When to Use a Full ORM

1. CRUD-heavy applications

Simple create, read, update, delete. Standard REST APIs. Admin dashboards.

prisma-crud.ts
const user = await prisma.user.create({
data: { email, name }
});
const users = await prisma.user.findMany({
where: { active: true },
include: { posts: true } // Auto-loaded relations
});

2. Rapid prototyping

Quick MVP, schema evolves frequently, need migration management.

3. Team prefers abstraction

Less SQL knowledge, consistent patterns, self-documenting schema.

4. Relationship management matters

Complex object graphs, lazy/eager loading, automatic cascade operations.

5. Schema-first design

Schema before implementation, single source of truth, Prisma schema as documentation.

Reddit insight: “I like ORMs for the DX on CRUDish operations, not the idea of switching dialects” - ORM DX shines for simple CRUD, but database portability is rarely relevant.

The Hybrid: Drizzle

Drizzle is a query builder with light ORM features:

  • SQL-like query builder syntax
  • TypeScript schema definition
  • Zero runtime dependencies
  • Optional ORM-like features (relations)

Reddit insight: “Drizzle is Query builder + light ORM, you can do pretty much anything using its query builder but lose ORM abilities like reference relations”

Code Examples: Same Query, Different Tools

kysely-join.ts
import { Kysely } from 'kysely';
interface Database {
users: { id: number; name: string; email: string };
posts: { id: number; title: string; author_id: number };
}
const db = new Kysely<Database>({ /* config */ });
const usersWithPosts = await db
.selectFrom('users')
.innerJoin('posts', 'posts.author_id', 'users.id')
.select(['users.name', 'posts.title'])
.where('users.id', '=', 1)
.execute();
// Complex query
const topAuthors = await db
.selectFrom('users')
.select([
'users.id',
'users.name',
sql<number>`count(posts.id)`.as('post_count')
])
.leftJoin('posts', 'posts.author_id', 'users.id')
.groupBy('users.id')
.orderBy('post_count', 'desc')
.limit(10)
.execute();
drizzle-query.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { users, posts } from './schema';
import { eq, sql } from 'drizzle-orm';
const db = drizzle(pool);
const usersWithPosts = await db
.select()
.from(users)
.innerJoin(posts, eq(users.id, posts.authorId))
.where(eq(users.id, 1));
const topAuthors = await db
.select({
id: users.id,
name: users.name,
postCount: sql<number>`count(${posts.id})`
})
.from(users)
.leftJoin(posts, eq(users.id, posts.authorId))
.groupBy(users.id)
.limit(10);
knex-query.ts
import knex from 'knex';
const db = knex({ client: 'pg', connection: '...' });
const usersWithPosts = await db('users')
.join('posts', 'users.id', 'posts.author_id')
.select('users.name', 'posts.title')
.where('users.id', 1);
// Raw SQL when needed
const result = await db.raw(`
SELECT u.id, u.name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.author_id
GROUP BY u.id
ORDER BY post_count DESC
LIMIT 10
`);
prisma-query.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const usersWithPosts = await prisma.user.findMany({
where: { id: 1 },
include: { posts: true }
});
// Complex query - raw SQL needed
const topAuthors = await prisma.$queryRaw`
SELECT u.id, u.name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.author_id
GROUP BY u.id
ORDER BY post_count DESC
LIMIT 10
`;
typeorm-query.ts
import { getRepository } from 'typeorm';
// Using relations
const usersWithPosts = await getRepository(User)
.find({
where: { id: 1 },
relations: ['posts']
});
// QueryBuilder for complex
const topAuthors = await getRepository(User)
.createQueryBuilder('user')
.leftJoin('user.posts', 'post')
.select(['user.id', 'user.name'])
.addSelect('COUNT(post.id)', 'postCount')
.groupBy('user.id')
.orderBy('postCount', 'DESC')
.limit(10)
.getRawMany();

Key insight: Query builders maintain SQL transparency. ORMs abstract it away. For complex queries, ORMs fall back to raw SQL anyway.

Decision Matrix

ScenarioRecommendedReasoning
High-performance APIKysely / DrizzleNo hidden queries
CRUD admin panelPrismaAuto-loaded relations
Existing complex schemaKysely / KnexPrecise SQL control
New project, SQL expertsDrizzleBest of both
Team with limited SQLPrisma / TypeORMAbstraction helps
Serverless / EdgeDrizzleZero deps, tiny bundle
Domain-Driven DesignMikro-ORMUnit of Work
Complex analyticsKyselyFull SQL features
Simple REST APIPrismaFastest development

Common Mistakes

Mistake 1: Choosing ORM for complex SQL

Reddit: “In Prisma you have limited interface that won’t be able to do lots of SQL features”

If you need window functions, CTEs, or complex joins, use a query builder.

Mistake 2: Choosing query builder for CRUD-heavy apps

Repetitive CRUD with query builder adds boilerplate. ORMs make simple operations trivial.

Mistake 3: Ignoring team expertise

SQL-skilled teams are more productive with query builders. SQL-new teams benefit from ORM abstraction.

Mistake 4: Believing database portability matters

Reddit: “In six years as primary dev at a SaaS company, switching databases has never been a topic of conversation”

Don’t choose ORM for database portability - it’s rarely needed.

2026 Landscape

Query Builders Rising:

  • Kysely: Pure type-safe query builder, excellent TypeScript inference
  • Drizzle: Hybrid query builder + light ORM, serverless-ready
  • Knex: Mature, battle-tested, extensive migration system

ORMs Remaining Strong:

  • Prisma: Best-in-class DX, schema-first, Prisma Studio
  • TypeORM: Enterprise features, Active Record + Data Mapper
  • Mikro-ORM: DDD-friendly, Unit of Work pattern

Trend: More developers choosing query builders (especially Drizzle/Kysely) for TypeScript projects.

The Reason

The key reason to understand this trade-off: control vs convenience.

Query builders give full SQL power with type safety but require SQL knowledge.

ORMs provide rapid CRUD development but can hit walls with complex queries.

Summary

In this post, I explained when to use query builder vs ORM in Node.js. The key point is: query builders give SQL control, ORMs give CRUD convenience.

For TypeScript projects in 2026, the trend favors query builders - especially Drizzle and Kysely - for type safety and SQL transparency.

Quick decision guide:

  • Query builder: SQL experts, complex queries, performance-critical, serverless
  • ORM: CRUD-heavy, rapid prototyping, team prefers abstraction, DX priority

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments