Node.js & TypeScript ORMs and Query Builders
Parent: JavaScript and Node.js · researched 2026-06-02T22:05:57.547Z· 16 sources · 7 concepts · skill nodejs-orm-query-builders
This reference is about the SQL data-access layer in TypeScript/Node.js: the
Overview
- This reference is about the SQL data-access layer in TypeScript/Node.js: the [source]
- library that sits between your code and a relational database (PostgreSQL, MySQL, [source]
- SQLite, SQL Server) and the patterns - migrations, N+1, transactions, pooling — [source]
- that apply no matter which library you pick. [source]
- The field splits along one axis: how much abstraction over SQL you want. [source]
- Full ORM (Prisma, TypeORM, Sequelize, MikroORM): models/entities, a [source]
- relation graph, change tracking, and a high-level query API. You think in [source]
- objects; the library writes SQL and maps rows back. [source]
- Query builder (Kysely; Drizzle's SQL-like API): a thin, type-safe wrapper [source]
- over SQL itself. You think in select/from/join; you get autocomplete and [source]
- compile-time column checking but no relation/identity abstraction. [source]
- Raw driver (pg, mysql2, better-sqlite3): you write SQL strings. Maximum [source]
- control, zero type-safety, most boilerplate. [source]
- Drizzle straddles the line - it markets as an ORM but is closer to a typed query [source]
- builder with an opt-in relational API. The single most consequential decision [source]
- is ORM-vs-builder-vs-driver; everything else (migrations, transactions) is then a [source]
- detail of the chosen tool. For MongoDB (Mongoose/ODM and document modeling) [source]
- this file does not apply - see mongodb-expert. For the driver-level pool [source]
- internals of one specific database, see that driver's own docs. [source]
1. Prisma — schema-first ORM with a generated client
- Prisma's center of gravity is a single declarative file, schema.prisma: [source]
- datasource, generator, and model blocks define the data model in Prisma's [source]
- own DSL (not TS). prisma generate reads it and emits Prisma Client - a [source]
- fully typed, autocompleting query API generated into node_modules. [source]
- Migrations: prisma migrate dev (development - diffs the schema, creates a [source]
- SQL migration, applies it, regenerates the client) vs prisma migrate deploy [source]
- (production/CI - applies already-committed migrations, never generates new ones). [source]
- prisma db push skips migration files for prototyping; prisma db pull [source]
- introspects an existing DB into the schema. [source]
- Type-safety: the client is generated from the schema, so model shapes, [source]
- select/include projections, and where filters are all statically typed — [source]
- a projection returns exactly the selected fields. [source]
- The engine model (important & changing): historically Prisma shipped a [source]
- Rust query engine binary that the JS client talked to. Prisma is removing [source]
- it - v7 (Nov 2025) makes a Rust-free client the default, using TS [source]
- driver adapters over the native Node driver. This changes pooling defaults [source]
- (now the driver's, not Prisma's) and improves edge/serverless fit. [source]
- Relation queries: include / select with nested writes; findMany, [source]
- create, nested connect/createMany. Prisma can emulate relations in the [source]
- app layer via relation mode (prisma vs foreignKeys) when the DB can't [source]
- enforce FKs (e.g. PlanetScale). [source]
- Fits: teams wanting maximum DX, type-safety, and a managed migration story; [source]
- Postgres/MySQL apps. Doesn't fit: cases needing hand-tuned SQL control, or [source]
- (pre-v7) edge runtimes where the engine binary was a problem. [source]
2. Drizzle ORM — SQL-first, no codegen, no runtime engine
- Drizzle defines the schema in TypeScript (pgTable/mysqlTable/sqliteTable [source]
- + column builders); that TS file is the single source of truth for both queries [source]
- and migrations. Its design claims: **zero dependencies, no code-generation step, [source]
- no runtime ORM engine** - a thin layer over the native driver that "always [source]
- outputs exactly 1 SQL query," making it lightweight and serverless/edge-ready. [source]
- Two query APIs: a SQL-like builder (db.select().from(users).where(...), [source]
- reads like SQL) and an opt-in relational queries API (`db.query.users. [source]
- findMany({ with: { posts: true } })`) for nested data without manual joins. [source]
- drizzle-kit is the CLI: generate (emit SQL migrations from schema diff), [source]
- migrate (apply them), push (prototype: push schema straight to DB), [source]
- pull (introspect), studio (GUI), plus check/up. [source]
- Transactions: await db.transaction(async (tx) => { ... }). [source]
- Fits: edge/serverless (Cloudflare Workers, Vercel Edge, Neon/Turso), teams [source]
- who want to see the SQL, bundle-size-sensitive deploys. Trade-off: less [source]
- hand-holding than Prisma; you own more of the modeling. [source]
3. Kysely — a type-safe query *builder* (not an ORM)
- Kysely is a type-safe SQL query builder inspired by Knex. It is explicitly [source]
- NOT an ORM and has no concept of relations - you write SQL semantics [source]
- (selectFrom, innerJoin, where, CTEs, window functions) and get full [source]
- compile-time checking and autocomplete derived from a Database interface [source]
- you declare (table → column-type map). [source]
- Type inference: column names, aliases, and result types are inferred from [source]
- subqueries, joins, and with (CTE) statements - the result type has exactly [source]
- the selected columns with correct types. [source]
- Composability: everything is an Expression; SelectQueryBuilder and [source]
- raw builders are themselves expressions, so you build **reusable query [source]
- fragments** and helpers. Query building and execution can be split. [source]
- The Database type is usually generated by kysely-codegen (official), [source]
- prisma-kysely, or introspection - keeping types in sync with the real DB. [source]
- Ships transactions (db.transaction().execute(...)), a migration [source]
- framework, and an sql template-tag escape hatch. Compiles to one statement. [source]
- When a builder beats an ORM: complex analytical SQL (window functions, [source]
- recursive CTEs, set operations), reporting, or when you want the DB schema — [source]
- not an object graph - to be the mental model, with zero hidden queries. [source]
4. TypeORM, Sequelize & MikroORM — entities, decorators, the AR/DM split
- These three are the mature, entity-based ORMs. [source]
- Active Record vs Data Mapper (the defining axis): in Active Record the [source]
- entity carries its own persistence methods (user.save(), User.find()); the [source]
- model extends a base class. In Data Mapper entities are "dumb" property bags [source]
- and persistence lives in separate repository classes (repo.save(user)), [source]
- which scales better in large apps. TypeORM uniquely supports both; [source]
- MikroORM is Data Mapper; Sequelize is Active Record. [source]
- TypeORM: @Entity/@Column/@OneToMany/@ManyToOne decorators (needs [source]
- experimentalDecorators/reflect-metadata); DataSource config; Repository [source]
- + QueryBuilder. Broad DB support; the de-facto NestJS default. [source]
- Sequelize: the oldest, most battle-tested (v6 mature, v7 modernizing TS). [source]
- Model classes, init/define, associations, include-based eager loading, [source]
- strong transactions, read replication, migrations via sequelize-cli. [source]
- MikroORM: implements Data Mapper + Unit of Work + Identity Map. The [source]
- Identity Map guarantees one in-memory instance per DB row within a request [source]
- (an in-request cache enabling cheap identity comparison and batched ops). [source]
- The Unit of Work tracks all changes via snapshot diffing and persists them [source]
- in one implicit transaction on em.flush() - you mutate entities and flush [source]
- once. Never share an EntityManager across requests; use RequestContext [source]
- (backed by AsyncLocalStorage) for request-scoped EMs. [source]
- Legacy note: TypeORM and Sequelize predate Prisma/Drizzle and carry larger [source]
- APIs and historically weaker end-to-end type-safety; choose them for ecosystem [source]
- maturity (Sequelize) or AR/DM flexibility & Nest integration (TypeORM). [source]
5. Migrations strategy (cross-cutting)
- A migration is a versioned, committed, ordered change to the DB schema. [source]
- Across all tools the same discipline applies: [source]
- Generate from a schema diff, commit the SQL, apply forward in CI/prod. [source]
- Prisma: migrate dev (gen+apply locally) → migrate deploy (apply in prod). [source]
- Drizzle: drizzle-kit generate → migrate. Kysely/TypeORM/Sequelize ship [source]
- their own runners. MikroORM has @mikro-orm/migrations. [source]
- push is not a migration. prisma db push / drizzle-kit push sync the [source]
- schema directly with no history - fine for prototyping, never for shared/ [source]
- prod environments (no rollback, no audit, easy to drift). [source]
- Migration maturity is a real selection factor: Prisma's shadow-DB-backed [source]
- drift detection and migrate deploy are the most opinionated/mature; Drizzle [source]
- and Kysely are lighter and give you raw SQL files you fully own. [source]
6. The N+1 problem, eager/lazy loading & DataLoader (cross-cutting)
- N+1 is the canonical data-layer performance bug: one query fetches N parent [source]
- rows, then the code triggers one query per parent for a relation (N more) — [source]
- 1 + N round-trips where 1–2 would do. It explodes silently under ORMs whose [source]
- lazy loading fetches a relation on property access, and under GraphQL [source]
- resolvers (one resolver per field per item). [source]
- Eager loading is the primary fix: tell the ORM to load the relation up [source]
- front in one query or a small fixed number - Prisma include, Drizzle with, [source]
- Sequelize include, TypeORM relations/leftJoinAndSelect, MikroORM [source]
- populate. Lazy loading fetches on demand (less memory, but the N+1 trap). [source]
- DataLoader is the batching fix when eager loading isn't structurally [source]
- possible (e.g. GraphQL): it coalesces the per-item key lookups within a tick [source]
- into one batched query and caches within the request. Create a **new [source]
- DataLoader per request** to avoid cross-user cache bleed. MikroORM has built-in [source]
- dataloaders. This is the standard GraphQL N+1 remedy. [source]
7. Transactions, pooling, raw SQL & repositories (cross-cutting)
- Transactions: every tool wraps a callback in a DB transaction - Prisma [source]
- $transaction (array form for batched independent ops, interactive form [source]
- for a callback with tx), Drizzle/Kysely db.transaction(...), TypeORM [source]
- dataSource.transaction / QueryRunner, MikroORM em.transactional (or the [source]
- implicit transaction em.flush() already provides). [source]
- Connection pooling at the data layer: the app holds a pool of DB [source]
- connections; size it to the database's connection ceiling, not to traffic. [source]
- In serverless, each function instance opens its own pool, so concurrent [source]
- invocations can exhaust DB limits - front the DB with an external pooler [source]
- (PgBouncer in transaction mode, Prisma Accelerate, Neon/Supabase poolers). [source]
- Prisma historically managed its own pool (connection_limit, pool_timeout); [source]
- with v7 driver adapters, pooling defaults come from the underlying driver. For [source]
- the driver-internal pool mechanics of one DB, see that driver's docs. [source]
- Raw-SQL escape hatch: keep one even with an ORM. Prisma $queryRaw / [source]
- $executeRaw (tagged-template, parameterized) and TypedSQL; Drizzle/Kysely [source]
- sql\...\` template tag; Sequelize sequelize.query; TypeORM query()`. [source]
- Always parameterize - string-concatenated raw SQL is SQL injection. [source]
- Repository pattern: wrap data access behind a repository interface so call [source]
- sites depend on a method (users.findActive()), not the ORM. TypeORM/MikroORM [source]
- ship Repository objects; with Prisma/Drizzle/Kysely you write thin repo [source]
- modules. This isolates the ORM choice and keeps it swappable. [source]
- Seeding: scripted insertion of baseline/dev data (Prisma prisma db seed [source]
- via a seed script; others run a plain script against the client) - keep it [source]
- idempotent and separate from migrations. [source]
Library comparison & selection
- Selection guidance [source]
- ORM vs builder vs raw driver: rich object graph, change tracking, fast CRUD [source]
- DX → ORM. Complex/analytical SQL with type-safety and no hidden queries → [source]
- query builder (Kysely / Drizzle SQL-API). One hot, perf-critical path or a [source]
- tiny script → raw driver. Most apps mix: an ORM for CRUD + a builder/raw [source]
- for the few heavy queries. [source]
- Prisma vs Drizzle vs Kysely vs TypeORM: [source]
- Best end-to-end DX + migration maturity → Prisma. [source]
- Edge/serverless, minimal bundle, SQL-first, no codegen → Drizzle. [source]
- You think in SQL, want builder ergonomics + types, no ORM magic → [source]
- NestJS, or you specifically want Active-Record or Data-Mapper choice → [source]
- TypeORM; classic battle-tested ecosystem → Sequelize; [source]
- Unit-of-Work / DDD identity semantics → MikroORM. [source]
Practical patterns
- **Prefer eager loading by default; reach for DataLoader only where you can't [source]
- eager-load** (GraphQL resolvers). Both beat lazy loading in a request hot path. [source]
- Use migrate deploy / drizzle-kit migrate in CI, never db push / [source]
- kit push, against shared environments - commit the generated SQL. [source]
- One pooler in serverless. Point the app at PgBouncer (transaction mode) or [source]
- Accelerate/Neon pooling so N function instances don't exhaust DB connections. [source]
- Keep a parameterized raw-SQL escape hatch for the queries the ORM models [source]
- awkwardly; don't fight the ORM for a window function - drop to sql\...\``. [source]
- Wrap the ORM in a repository module so the data layer stays swappable and [source]
- call sites don't import the client everywhere. [source]
- Make seeds idempotent (upsert, not blind insert) so re-running is safe. [source]
- Pin to interactive transactions when later writes depend on earlier reads [source]
- within the same atomic unit (Prisma $transaction(async tx => …)). [source]
Anti-patterns
- N+1 queries. Iterating parents and touching a lazy relation per item. [source]
- Symptom: a flood of near-identical single-row SELECTs in the query log. Fix: [source]
- eager-load the relation (include/with/relations/populate) or batch with [source]
- DataLoader. The cardinal data-layer performance bug. [source]
- db push / kit push to production. No history, no rollback, silent drift. [source]
- Use generated, committed migrations everywhere shared. [source]
- String-concatenated raw SQL. queryRaw("... " + userInput) is SQL [source]
- injection - always use the parameterized tagged-template form. [source]
- A pool per serverless invocation with no external pooler - exhausts the [source]
- DB's max_connections under concurrency. Front it with PgBouncer/Accelerate. [source]
- **SELECT * via the ORM when you need three columns** - fetch only what you [source]
- project (select) to cut payload and avoid over-fetching. [source]
- **Sharing one long-lived EntityManager/identity-mapped context across [source]
- requests** (MikroORM/TypeORM) - leaks state between users; use request-scoped [source]
- contexts (RequestContext / AsyncLocalStorage). [source]
- Treating Kysely or Drizzle's SQL API like an ORM - there's no relation [source]
- graph or identity map; you compose SQL, you don't navigate objects. [source]
Troubleshooting
- Mysterious burst of identical SELECTs → N+1; turn on query logging, [source]
- eager-load or add DataLoader. [source]
- "too many connections" / pool timeout under load (esp. serverless) → pool [source]
- sized above the DB ceiling × instance count; reduce per-instance limit and add [source]
- an external transaction-mode pooler. [source]
- Prisma types out of date after a schema edit → re-run prisma generate [source]
- (it's a generated client; the build won't pick up changes otherwise). [source]
- PgBouncer transaction mode breaking prepared statements / Prisma → set [source]
- pgbouncer=true on the connection string and follow the pooler-mode guidance; [source]
- prepared-statement caching conflicts with transaction-pooling. [source]
- Kysely/Drizzle "column doesn't exist" only at runtime → the generated [source]
- Database type / TS schema drifted from the DB; re-introspect / re-run [source]
- kysely-codegen / drizzle-kit pull. [source]
- MikroORM changes not saved → you forgot em.flush(); the Unit of Work [source]
- persists on flush, not on mutation. [source]
- Migration "drift detected" (Prisma) → the DB diverged from migration [source]
- history (a manual change or a db push); resolve with migrate diff / [source]
- baselining rather than another push. [source]
References
- Prisma - Prisma Client overview (generated client, type-safe queries): https://www.prisma.io/docs/orm/prisma-client [source]
- Prisma - Prisma Migrate (migrate dev / deploy, db push): https://www.prisma.io/docs/orm/prisma-migrate [source]
- Prisma - Connection pool (connection_limit, serverless, PgBouncer, v7 driver adapters): https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/databases-connections/connection-pool [source]
- Prisma - Relation mode (prisma vs foreignKeys): https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/relation-mode [source]
- Prisma - Release notes / changelog (v7 Rust-free client): https://www.prisma.io/changelog [source]
- Drizzle ORM - Why Drizzle / overview (no codegen, no runtime engine, 1 query, serverless): https://orm.drizzle.team/docs/overview [source]
- Drizzle ORM - Migrations & drizzle-kit (generate/migrate/push/pull): https://orm.drizzle.team/docs/migrations ; https://orm.drizzle.team/docs/kit-overview [source]
- Drizzle ORM - Query data & relational queries (with): https://orm.drizzle.team/docs/data-querying ; https://orm.drizzle.team/docs/rqb-v2 [source]
- Kysely - Introduction (type-safe builder, not an ORM, no relations): https://kysely.dev/docs/intro [source]
- Kysely - Expressions (composable query building blocks) & Relations recipe: https://kysely.dev/docs/recipes/expressions ; https://kysely.dev/docs/recipes/relations [source]
- TypeORM - Active Record vs Data Mapper: https://typeorm.io/docs/guides/active-record-data-mapper/ [source]
- TypeORM - Entities & decorators; Repository: https://typeorm.io/docs/entity/entities/ ; https://typeorm.io/docs/working-with-entity-manager/working-with-repository/ [source]
- Sequelize - Eager loading (include) & Associations: https://sequelize.org/docs/v6/advanced-association-concepts/eager-loading/ ; https://sequelize.org/docs/v6/core-concepts/assocs/ [source]
- MikroORM - Unit of Work & transactions; Identity Map & request context: https://mikro-orm.io/docs/unit-of-work ; https://mikro-orm.io/docs/identity-map [source]
- MikroORM - Dataloaders (built-in N+1 batching): https://mikro-orm.io/docs/dataloaders [source]
- DataLoader - Solving the N+1 problem (GraphQL.js guide): https://www.graphql-js.org/docs/n1-dataloader/ [source]
Children
- Prisma — schema-first ORM with a generated client (frontier)
- Drizzle ORM — SQL-first, no codegen, no runtime engine (frontier)
- Kysely — a type-safe query builder (not an ORM) (frontier)
- TypeORM, Sequelize & MikroORM — entities, decorators, the AR/DM split (frontier)
- Migrations strategy (cross-cutting) (frontier)
- The N+1 problem, eager/lazy loading & DataLoader (cross-cutting) (frontier)
- Transactions, pooling, raw SQL & repositories (cross-cutting) (frontier)
Frontier under this node: Drizzle ORM — SQL-first, no codegen, no runtime engine, Kysely — a type-safe query builder (not an ORM), Migrations strategy (cross-cutting), Prisma — schema-first ORM with a generated client, The N+1 problem, eager/lazy loading & DataLoader (cross-cutting), Transactions, pooling, raw SQL & repositories (cross-cutting), TypeORM, Sequelize & MikroORM — entities, decorators, the AR/DM split