ORMs vs Raw SQL
An ORM (Object-Relational Mapper) translates between your programming language's objects and database tables. Instead of writing SQL, you write code like User.find(id) or users.filter(active=true). The ORM generates SQL behind the scenes. This debate is recurring in development, and the answer is usually "use both."
What ORMs Are
Prisma is TypeScript-first and generates type-safe queries. Drizzle is lighter and SQL-like. SQLAlchemy is Python's standard. ActiveRecord powers Ruby on Rails. Eloquent is Laravel's. Hibernate is Java's. Every language has ORMs.
ORMs provide a consistent interface across databases. Code written with an ORM can theoretically switch databases. In practice, you'll write database-specific SQL when you need to, but the abstraction handles common queries.
Benefits of ORMs
You write code in your language. No SQL strings scattered throughout your application. ORMs generate migrations, creating a changelog of schema changes. Type safety: Prisma generates types from your schema, so your IDE knows what fields exist on each model. This catches errors before runtime.
ORMs reduce boilerplate. CRUD operations are one-liners instead of writing SELECT/INSERT/UPDATE/DELETE statements. For the common case of standard queries, ORMs are more productive.
Downsides of ORMs
ORMs can hide performance problems. A query that looks innocent might generate inefficient SQL. You might accidentally cause N+1 queries. The ORM abstraction obscures what's actually happening.
Some queries are hard to express in an ORM. Complex JOINs with aggregations, window functions, and CTEs are tedious. The abstraction works best for simple queries and breaks down for complex ones.
ORMs limit you to the database features the ORM supports. PostgreSQL-specific features like window functions, JSON operators, and full-text search are often unsupported or awkward to use.
Prisma Specifically
Prisma is the most popular ORM in the Node.js ecosystem. It's TypeScript-first, generating types from your schema file. Queries are type-safe—your IDE completes field names and validates types.
Prisma's migration system is excellent. prisma migrate dev generates migrations from schema changes. prisma db push quickly syncs your schema to development. This is much better than manually writing migrations.
Downsides: Prisma's query language has limits. Complex queries require raw SQL. Prisma is newer, with less ecosystem maturity than some alternatives. Some developers find the Prisma Client API verbose compared to hand-written SQL.
Drizzle as an Alternative
Drizzle is lighter than Prisma. It stays closer to SQL. Drizzle queries look more SQL-like, making the generated SQL more obvious. It's newer and has less ecosystem support, but for developers who prefer control and visibility, Drizzle is compelling.
Raw SQL: When and Why
For complex queries—bulk operations, analytics queries with multiple JOINs and aggregations, queries using PostgreSQL-specific features—raw SQL is often clearer and faster.
Always use parameterised queries. Never concatenate user input. const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]) is safe. const user = await db.query('SELECT * FROM users WHERE id = ' + userId) is vulnerable to SQL injection.
The Hybrid Approach
The pragmatic approach: use an ORM for most CRUD operations. Drop to raw SQL for complex queries and performance-critical paths. Prisma allows this with prisma.$queryRaw(). This combines ORM convenience with SQL flexibility.
As your application grows, you'll find that most queries are simple CRUD that ORMs handle well, but a handful are complex. Handle both without forcing one paradigm everywhere.
Understanding Generated SQL
Whether you use an ORM or write SQL directly, you must understand what queries actually execute. Enable query logging. Prisma has query events. Drizzle logs queries in development. See what your ORM generates. If a Prisma query generates three JOINs when one would suffice, fix it.
| Aspect | ORM | Raw SQL |
|---|---|---|
| Learning curve | Moderate, API-specific | Steeper, but universal |
| Simple queries | Easy, concise | Verbose but clear |
| Complex queries | Awkward or impossible | Natural and explicit |
| Type safety | Strong in TypeScript ORMs | Must manage manually |
| Performance visibility | Low, hidden generated SQL | High, you write the SQL |
| Migrations | Automatic from schema | Manual management |
| Database features | Limited to ORM support | Full feature access |
| Flexibility | Constrained by design | Total control |
| Best for | CRUD and standard patterns | Complex and analytical queries |