ORMs Are Good, Actually.
Shayan

Shayan @shayy

About: Building UserJot in Public

Location:
Maryland, United States
Joined:
Jan 14, 2025

ORMs Are Good, Actually.

Publish Date: Jun 27
15 3

When I started building UserJot, I decided to go all-in on Postgres: full-text search, pgvector, LISTEN/NOTIFY, the works. That meant I needed a solid ORM.

UserJot Dashboard

I had two options: Drizzle and Prisma. Drizzle looked promising but was still new with some rough edges. Plus, at the time, LLMs couldn't write Drizzle code reliably (they can now, but this mattered back then). So I went with Prisma, despite never using it in production before.

Months later, it's been one of the best technical decisions I've made. Here's why.

The Schema Language Actually Makes Sense

I know a lot of people prefer Drizzle over Prisma specifically because Drizzle lets you define your schema in TypeScript. And I get it - keeping everything in one language sounds appealing. Why learn another syntax when you can just write TypeScript?

But after months of working with Prisma's schema language, I've come to really appreciate the dedicated DSL. Here's what a typical model looks like:

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        String  @id @default(cuid())
  title     String
  content   String?
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  String
}
Enter fullscreen mode Exit fullscreen mode

Compare this to defining the same schema in TypeScript with decorators or builder patterns. It's just so much cleaner. No imports, no boilerplate, no verbose method chaining. Just the data structure.

The thing is, your database schema is fundamentally different from your application code. It's declarative, not imperative. Having a dedicated language for it makes sense in the same way that SQL makes sense for queries, or CSS makes sense for styles.

Plus, the Prisma schema file becomes this single source of truth that's incredibly easy to scan. When I'm trying to understand the data model of a project, I can open one file and immediately see all the relationships, constraints, and defaults. Try doing that with TypeScript schemas scattered across multiple files with various decorators and type definitions.

After working with it for months, going back to TypeScript schema definitions feels like writing XML by hand. Sometimes a DSL is the right tool for the job.

Type Safety That Actually Works

This is where Prisma shines. That schema above? It generates fully typed queries, inputs, and outputs. Everything.

When I write:

const user = await prisma.user.create({
  data: {
    email: 'user@example.com',
    name: 'New User',
    posts: {
      create: {
        title: 'My First Post',
        content: 'Hello world!'
      }
    }
  },
  include: {
    posts: true
  }
});
Enter fullscreen mode Exit fullscreen mode

TypeScript knows exactly what user contains. Change a field name in the schema? Your IDE lights up with errors everywhere that field is used. It's the kind of developer experience that makes you wonder why we settled for less for so long.

The Escape Hatches Are Actually Good

Here's where most ORMs fall apart: when you need to do something they didn't anticipate. Maybe you're using a Postgres extension like pgvector, or you need a specific query optimization the ORM can't generate.

Prisma handles this gracefully. You've always been able to drop down to raw SQL, but they recently introduced TypedSQL, and it's a game-changer.

Here's how it works. You write your SQL in a .sql file:

-- prisma/sql/searchPosts.sql
-- @param {String} $1:searchTerm
SELECT p.*, u.name as author_name,
       ts_rank(to_tsvector('english', content), query) as rank
FROM posts p
JOIN users u ON p."authorId" = u.id,
     plainto_tsquery('english', $1) query
WHERE to_tsvector('english', content) @@ query
ORDER BY rank DESC
LIMIT 10
Enter fullscreen mode Exit fullscreen mode

Then in your TypeScript code:

import { searchPosts } from '@prisma/client/sql';

const results = await prisma.$queryRawTyped(searchPosts(searchTerm));
// results is fully typed, including the rank field!
Enter fullscreen mode Exit fullscreen mode

Raw SQL with full type safety. Your complex queries, Postgres extensions, and performance optimizations all get the same developer experience as regular Prisma queries. It's brilliant.

They Fixed the N+1 Problem (Finally)

Let's address the elephant in the room. Prisma used to handle joins in their query engine rather than at the database level, which could lead to N+1 query problems. It was a valid criticism, and honestly, it was one of the things that made me hesitate initially.

But they fixed it. Since version 5.8.0, Prisma supports database-level joins through their relationLoadStrategy option. You can now choose between:

  • join (default): Uses proper database joins (LATERAL JOIN on PostgreSQL, correlated subqueries on MySQL) and fetches everything in a single query
  • query: The old behavior, multiple queries joined at the application level

The new join strategy doesn't just reduce the number of queries. It also uses JSON aggregation at the database level. This means Postgres or MySQL builds the JSON structure that Prisma returns, saving computation on your application server.

To use it, you need to enable the preview feature:

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["relationJoins"]
}
Enter fullscreen mode Exit fullscreen mode

The performance difference is noticeable, especially for queries with nested relations. It's one of those updates that shows they're listening to the community and addressing real production concerns.

The Developer Experience Delivers

So many tools promise "better DX" and deliver marginal improvements at best. Prisma actually delivers.

Running migrations is dead simple. Just make your schema change and run prisma migrate dev --name add_user_role. It generates the SQL, applies it, and updates your TypeScript types all in one go.

What really gets me is how the whole workflow just flows. You change a field name in your schema, run the migration, and suddenly TypeScript is yelling at you everywhere that field was used in your code. It's not just helpful; it makes refactoring feel safe.

If you're working with an existing database, you can point Prisma at it and it'll generate the entire schema for you. No manual mapping, no guessing at types. It just works.

It's the accumulation of these small wins that makes Prisma a joy to work with. When I'm building features for UserJot, I'm thinking about the product, not fighting with my ORM.

The Trade-offs (Because Nothing's Perfect)

Let's be really transparent about the downsides, because choosing an ORM is a big decision. It touches almost every part of your codebase, and migrating away from one is painful - even with good abstractions, you're looking at a massive refactor.

Here's what you should consider:

Lock-in is real: Once you build your app with Prisma, you're pretty much committed. This isn't something you swap out on a whim.

Bundle size: The client isn't tiny. If you're building something where every KB matters, this might be a concern.

Learning curve: Yes, there's another syntax to learn. Though honestly, it's one of the easier tools I've picked up.

They're VC-backed: Prisma is a venture-funded company. While I've talked with the founder Søren a few times and trust the team's direction, it's still something to consider. Open source or not, there's always risk when your core infrastructure depends on a VC-backed company's decisions.

For UserJot, these trade-offs have been worth it. The productivity gains are real. I'm shipping features faster and with more confidence than I would with raw SQL or other ORMs. But you need to evaluate these risks for your own situation.

The lock-in especially is something to think hard about. Make sure you're comfortable with the trade-offs before going all-in.

Should You Use It?

There's a real stigma against ORMs these days. "Just write raw SQL" is almost a meme at this point. And I get it - I've been burned by bad ORMs too.

But here's the nuance that gets lost in the debate: when you pick an ORM purposefully, understand its strengths and limitations, and choose one with good escape hatches, you get something really valuable. You get the convenience and safety for 95% of your queries, and the flexibility to write raw SQL for that remaining 5% where you need something specific.

Prisma hits this balance perfectly for me. The type safety catches bugs before they happen. The schema language keeps things clean. The migrations just work. And when I need to write a complex Postgres-specific query, TypedSQL lets me do exactly that without losing any of the benefits.

Your mileage may vary. It depends on your language, your database, your team's preferences. But don't dismiss ORMs just because it's fashionable to hate on them. When chosen thoughtfully, they can be a massive productivity multiplier.

ORMs don't have to suck. Prisma proves it.


P.S. If you're curious what I'm building with all this Prisma goodness, I'm working on UserJot, a simple way to collect user feedback and manage roadmaps. Come check it out!

UserJot feedback board with user discussions

Comments 3 total

  • Jonas Scholz
    Jonas ScholzJun 27, 2025

    drizzle got better twitter game tho

    • Shayan
      ShayanJun 27, 2025

      lol that's actually true

  • 徐伟
    徐伟Jun 28, 2025

    lol that's actually true

Add comment