ORMs, Raw SQL, and Why Prisma Won
Understand what an ORM solves, where raw SQL still wins, and why Prisma became the default choice in TypeScript projects.
Written by the RadarTrek editorial team · June 2026
The problem with raw SQL in TypeScript
When you write raw SQL inside TypeScript, your editor has no idea what the query will return. You get back `any[]`, you cast it, you ship it — and typos, wrong column names, or schema changes silently break things at runtime. The compiler cannot help you.
Raw SQL — zero type safety
import { pool } from './db'
// TypeScript trusts you completely. That's a problem.
const rows = await pool.query('SELECT id, emial, name FROM users WHERE id = $1', [userId])
// ^^^^^ typo — will crash at runtime, not compile time
const user = rows.rows[0] as { id: number; email: string; name: string }What an ORM actually does
An ORM (Object-Relational Mapper) sits between your application code and the database. You describe your data as objects or types, and the ORM translates your method calls into SQL. The key benefit is that your data model is defined once — in code — and the ORM ensures queries match it.
The translator analogy
Think of an ORM like a professional translator at a business meeting. You speak your language (TypeScript), the translator converts it to the language the database speaks (SQL), and brings the response back in a form you understand — with all the meaning preserved.
The ORM landscape before Prisma
TypeORM and Sequelize dominated the Node.js world for years. Both worked, but both leaked abstractions badly in TypeScript: you annotated classes with decorators, but the types were still mostly inferred as `any`. You won expressiveness but lost the type safety TypeScript is supposed to give you.
- TypeORM — decorator-based, mature, but type inference is shallow — findOne() returns Entity | undefined but field types are not verified
- Sequelize — JavaScript-first, verbose TypeScript support added later as an afterthought
- Knex.js — query builder, not an ORM — gives you type-safe query construction but you still write the column names by hand
What Prisma does differently
Prisma generates its own TypeScript client from your schema.prisma file at build time. The generated types are exact: every model, every field, every relation is typed. When you call `prisma.user.findUnique({ where: { id: 1 } })`, TypeScript knows the exact shape of what comes back — no casting required.
Prisma — fully typed query result
import { prisma } from '@/lib/prisma'
// TypeScript knows this returns: { id: number; email: string; name: string } | null
const user = await prisma.user.findUnique({ where: { id: userId } })
// Autocomplete works. Typos are compile errors.
console.log(user?.email) // ✓
console.log(user?.emial) // ✗ Property 'emial' does not existThe schema is the source of truth
In Prisma, you never manually write type definitions for your database models. They're generated automatically every time you run `npx prisma generate`. Your schema.prisma file IS your single source of truth for both the database structure and the TypeScript types.
When raw SQL still wins
Prisma is not always the right tool. For analytical queries — complex window functions, recursive CTEs, multi-table aggregations — the ORM abstraction can produce inefficient SQL or simply not support the construct. Prisma provides `$queryRaw` for exactly these cases. Use Prisma for 90% of your queries and raw SQL for the remaining 10% that need it.
The schema.prisma file — a quick look
Everything in Prisma starts with a single file: schema.prisma. It defines your data source (which database, connection string), the Prisma Client generator (which produces the TypeScript types), and your models (tables). You will write this file in the next lesson.
schema.prisma — the skeleton
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
}Key ideas from this lesson