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:
| Aspect | Query Builder | ORM |
|---|---|---|
| Abstraction Level | Low (SQL-close) | High (domain model) |
| SQL Knowledge Required | Essential | Helpful |
| Flexibility | Maximum | Limited by features |
| Boilerplate | More explicit | Less for simple ops |
| Relationship Management | Manual joins | Auto-loaded relations |
| Performance Control | Full visibility | Abstracted |
| Learning Curve | SQL transfers | ORM-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:
| Tool | Key Features | Best For |
|---|---|---|
| Kysely | Full type inference, SQL-like API | TypeScript-first |
| Drizzle | Query builder + light ORM, zero deps | Serverless, SQL experts |
| Knex | Mature, migration system | Existing 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:
| Tool | Pattern | Best For |
|---|---|---|
| Prisma | Schema-first, Client API | Rapid development |
| TypeORM | Active Record / Data Mapper | Enterprise flexibility |
| Mikro-ORM | Data Mapper, Unit of Work | DDD, complex domains |
When to Use a Query Builder
1. Complex SQL needed
CTEs, window functions, complex joins, database-specific features.
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.
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
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 queryconst 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();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);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 neededconst 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`);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 neededconst 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`;import { getRepository } from 'typeorm';
// Using relationsconst usersWithPosts = await getRepository(User) .find({ where: { id: 1 }, relations: ['posts'] });
// QueryBuilder for complexconst 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
| Scenario | Recommended | Reasoning |
|---|---|---|
| High-performance API | Kysely / Drizzle | No hidden queries |
| CRUD admin panel | Prisma | Auto-loaded relations |
| Existing complex schema | Kysely / Knex | Precise SQL control |
| New project, SQL experts | Drizzle | Best of both |
| Team with limited SQL | Prisma / TypeORM | Abstraction helps |
| Serverless / Edge | Drizzle | Zero deps, tiny bundle |
| Domain-Driven Design | Mikro-ORM | Unit of Work |
| Complex analytics | Kysely | Full SQL features |
| Simple REST API | Prisma | Fastest 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:
- 👨💻 Reddit Discussion: What's the best nodejs ORM in 2026?
- 👨💻 Kysely GitHub
- 👨💻 Drizzle ORM Documentation
- 👨💻 Knex Documentation
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments