I've been building a SaaS product for three years. The database has over 100 million rows across hash-partitioned PostgreSQL tables. And I don't use an ORM.
Every few months someone on Twitter declares ORMs dead. Then someone else defends them. The takes are always abstract. Here's mine, from actually living with the alternative.
Why I skipped the ORM
When I started building my app, I evaluated Prisma seriously. Typed queries, clean migrations, great DX. But I was building something that would need hash partitioning, complex analytical queries, and tight control over the SQL being generated. Prisma's schema language doesn't support partitioning at all, and I knew I'd be reaching for raw SQL constantly. Prisma's $queryRaw loses all type safety (yes, Prisma has TypedSQL in preview now, but it didn't exist when I started, and it still requires a generation step for each query).
So I went with Kysely from day one. It's a TypeScript query builder, not an ORM. It doesn't pretend to abstract away SQL. It just makes SQL type-safe.
What a query builder actually gives you
You write SQL. You just write it in TypeScript:
const results = await db
.selectFrom('analytics_events')
.where('account_id', '=', accountId)
.where('created_at', '>=', startDate)
.select(['event_type', 'page_url', 'duration', 'metadata'])
.orderBy('duration', 'desc')
.limit(100)
.execute();
That's it. No model definitions. No lazy loading gotchas. No N+1 surprises. The generated SQL is exactly what you'd write by hand, and results is fully typed.
For the type safety layer, I use Kanel to auto-generate TypeScript interfaces directly from the PostgreSQL schema. Schema changes? Run one command. Types update automatically. No manual type definitions drifting out of sync.
CTEs, window functions, INSERT ... ON CONFLICT, lateral joins: it all just works because you're writing SQL, not fighting an abstraction. And because PostgreSQL handles partitioning transparently at the database level, Kysely doesn't need any special partition awareness. It queries the parent table, PostgreSQL routes to the right partition. No ORM migration system trying to fight your DDL.
The honest trade-offs
I'm not going to pretend there are no downsides.
Migrations are manual. No prisma migrate dev magic. I write migration files by hand with Kysely's migration API. It's more work. But I also know exactly what's happening to my schema, which matters a lot when you're partitioning large tables.
Relations aren't automatic. Want to fetch a user with their projects and tags? That's a join you write yourself, not a .include({ projects: true }). For simple CRUD apps, this is genuinely slower to develop.
The ecosystem is smaller. Prisma has a massive community, extensions, and tooling. Kysely's community is growing fast but it's not there yet.
When ORMs are still the right call
If you're building a CRUD app, a content site, or a prototype: use Prisma. Seriously. The DX is incredible and the abstractions hold up fine for straightforward data access.
ORMs break down when:
- You need database-specific features (partitioning, materialized views, custom indexes)
- Your queries are complex enough that you'd reach for raw SQL regularly
- Performance matters and you need to control the exact SQL being generated
- Your schema evolves in ways the ORM migration system can't express
If three or more of those describe your situation, a query builder will save you pain long-term.
The full-stack type safety bonus
One more thing that changed my architecture. I paired Kysely with ts-rest for type-safe API contracts. ts-rest lets you define your API once with Zod schemas, and both your Express backend and Vue frontend get full type checking on requests and responses. Refactor an endpoint and TypeScript tells you everywhere that breaks.
The combination of Kysely + Kanel + ts-rest means the entire data path from PostgreSQL to the browser is type-checked at compile time. Kanel handles database-to-TypeScript. ts-rest handles API-to-frontend. Runtime type errors between frontend and backend basically disappeared.
The real lesson
The question isn't "ORM vs no ORM." It's "how much abstraction does your project actually need?"
For three years I've been running Content Raptor on this stack. Hash-partitioned tables, 100M+ rows, complex analytical queries, multiple backend services. Zero ORM overhead, full type safety.
The best tool is the one that gets out of your way when things get complicated. ORMs are great until they aren't. Query builders are slightly more work upfront but they never hit a wall.
Pick based on where your project is actually going, not where it is today.
What's your experience? Have you hit the wall with an ORM and switched to something else? Or have modern tools like Prisma's TypedSQL and Drizzle (which blurs the line between query builder and ORM) closed the gap? I'd love to hear real production stories in the comments.
Top comments (0)