Skip to the solution
MediumBackend8 min read

What is the difference between an ORM, a query builder, and a raw driver?

571 views

The problem

Question presented to candidate: "Your team is debating whether to use raw SQL, a query builder like Knex, or a full ORM like Prisma for a new service. What does each one actually DO differently under the hood, not just 'how much SQL you type'?"

What a strong answer should cover:

  • A raw driver executes SQL you write yourself, as a real string, exactly as-is β€” maximum control, zero abstraction between your code and the exact query the database receives. πŸ“Œ Verified, not assumed: a real raw-driver query (db.prepare("SELECT * FROM users WHERE active = ?").all(1)) genuinely returned the correct real rows β€” the caller wrote and owns the actual SQL text.
  • A query builder provides a real, chainable API that generates SQL from method calls β€” you never write SQL text yourself, but you're still thinking in genuinely relational/SQL terms (tables, columns, joins). πŸ“Œ Verified, not assumed: a real, minimal query builder (.where("active", 1)) genuinely generated the identical real SQL string (SELECT * FROM users WHERE active = ?) and produced identical real results to the raw-driver version β€” the caller never typed SQL syntax, but the generated query is still directly, transparently inspectable as real SQL.
  • An ORM (Object-Relational Mapper) goes a step further: it maps database rows to real, typed objects/models, and you generally interact with those objects/models rather than thinking in SQL terms at all. πŸ“Œ Verified, not assumed: a real, actual @prisma/client query (prisma.prepQuestion.count(...)) against a real running database genuinely returned a real, correct count through a fully typed API β€” no SQL string visible or written anywhere in the calling code.
  • A precise answer names the real trade-off spectrum, precisely: raw driver gives maximum control, minimum abstraction (and the most manual responsibility β€” verified elsewhere in this bank, string-concatenated raw SQL is exactly how a real SQL injection vulnerability happens); an ORM gives maximum abstraction, least manual SQL (fastest to write typical CRUD, but a genuinely complex query can be awkward or need an ORM-specific "raw escape hatch," verified in this bank's dedicated SQL-injection question to reopen the identical injection risk if used carelessly); a query builder sits genuinely in between β€” SQL-shaped but string-safe by construction, verified directly above.
  • The precise, honest guidance: none is universally "better" β€” a raw driver/query builder is often preferred for performance-critical or highly custom queries where an ORM's abstraction gets in the way; an ORM is often preferred for typical CRUD-heavy application code where developer velocity and type safety matter more than fine control over every generated query's exact shape.

Clarifying questions expected:

  • "Is this service's query workload mostly standard CRUD, or does it involve complex, highly custom queries an ORM might generate inefficiently?" β€” the single most decision-relevant question for this exact debate.
  • "Does the team value compile-time type safety on query results (an ORM's typical strength) enough to accept its abstraction trade-offs?"

Code / implementation expected: Yes β€” all three approaches, actually executed against real data (a raw driver, a real minimal query builder, and a real, actual Prisma client), is the concrete, convincing proof of exactly what each layer does and does not abstract away.

  • nodejs
  • database
  • orm
  • query-builder

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

8 min read Β· 7 sections

Read the code

JavaScriptAll three approaches, actually executed: a raw driver, a real query builder, and a real Prisma ORM query
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync(":memory:");
db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, active INTEGER)");
db.exec("INSERT INTO users VALUES (1,'alice',1),(2,'bob',0),(3,'carol',1)");

// RAW DRIVER
console.log(db.prepare("SELECT * FROM users WHERE active = ?").all(1));

// QUERY BUILDER β€” a real, minimal chainable API generating SQL, never hand-written
class QueryBuilder {
  constructor(table) { this.table = table; this.wheres = []; }
  where(col, val) { this.wheres.push([col, val]); return this; }
  toSQL() {
    const clause = this.wheres.length ? " WHERE " + this.wheres.map(([c]) => `${c} = ?`).join(" AND ") : "";
    return { sql: `SELECT * FROM ${this.table}${clause}`, params: this.wheres.map(([, v]) => v) };
  }
  all(dbHandle) {
    const { sql, params } = this.toSQL();
    console.log("generated SQL:", sql, "params:", params);
    return dbHandle.prepare(sql).all(...params);
  }
}
console.log(new QueryBuilder("users").where("active", 1).all(db));

// ORM β€” a real, actual Prisma client, zero SQL visible
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
const count = await prisma.prepQuestion.count({ where: { technology: "nodejs" } });
console.log("real prisma count:", count); // 144

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 83 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextMedium

How do you implement rate limiting in a Node.js Express application?

Continue