Should You Stop Using Prisma? Why Database ORMs Might Be the Worst Thing That Happened to Backend Development | by Sohail Saifi

Press enter or click to view image in full size

I need to tell you something that might be controversial: Prisma, the darling of the TypeScript world, might be holding your application back.

After three years of using Prisma in production, debugging performance issues that shouldn’t exist, and watching our serverless costs spiral out of control, I’ve come to a painful realization. The same tool that promised to make database development easier has become our biggest bottleneck.

But this isn’t just about Prisma. This is about a fundamental problem with how we’ve been thinking about database access in modern applications. ORMs promised us productivity and type safety, but they delivered complexity and performance issues we never asked for.

Here’s my journey from ORM evangelist to someone who thinks we took a seriously wrong turn.

The Prisma Promise (And Why We All Fell for It)

Let’s be honest — Prisma’s marketing is brilliant. Type-safe database access! Automatic migrations! No more SQL! A single schema file that generates everything you need!

When I first saw this code, I was sold:

const users = await prisma.user.findMany({
where: {
email: {
contains: 'john'
}
},
include: {
posts: {
select: {
title: true,
createdAt: true
}
}
}
})

Clean, readable, type-safe. What could go wrong?

Everything.

The Performance Reality Check

Here’s what they don’t tell you about Prisma: it’s designed to be convenient, not fast.

Let’s talk numbers. According to benchmarks, Drizzle’s bundle size is about 1.5 MB, while Prisma’s bundle size is about 6.5 MB. In serverless environments, this matters. A lot.

But bundle size is just the beginning. The real performance killer is what happens at runtime.

The N+1 Problem on Steroids

Remember that innocent-looking query above? Here’s what it actually does:

-- First query
SELECT * FROM users WHERE email LIKE '%john%';

-- Then for EACH user found:
SELECT * FROM posts WHERE userId = 1;
SELECT * FROM posts WHERE userId = 2;
SELECT * FROM posts WHERE userId = 3;
-- ... and so on

One logical query becomes dozens of database round trips. In one of our applications, what should have been a single JOIN query was generating 47 separate SQL statements.

We moved to TanStack Query for our frontend state management to avoid this exact problem, only to recreate it in our backend with Prisma.

The Serverless Nightmare

Our AWS Lambda cold starts went from 200ms to 2.5 seconds after introducing Prisma. Why? Because Prisma needs to:

  1. Load the Rust-based query engine binary
  2. Start an internal GraphQL server
  3. Initialize connection pooling
  4. Generate the client interface

All of this happens before your function can even start processing the request.

As one developer put it: “Prisma happens to have a lot of content that you can find on this topic specifically, but the problem stems from cold starts in certain serverless environments like AWS Lambda. With Drizzle being such a lightweight solution, the time required to load and execute a serverless function or Lambda will be much quicker than Prisma.”

The Developer Experience Mirage

Prisma’s biggest selling point is Developer Experience (DX). But here’s the dirty secret: great DX for simple things often means terrible DX for complex things.

The Schema Lock-In

Everything in Prisma revolves around the schema.prisma file. It’s your single source of truth, they say. But what happens when you need to do something that doesn’t fit neatly into Prisma’s DSL?

Want to use database-specific features like PostgreSQL’s JSONB operators? Good luck. Need to write a complex query with CTEs? You’re back to raw SQL, but now you’re mixing paradigms and losing all the type safety you supposedly gained.

As one frustrated developer noted: “All the boilerplate and the files for redux remain the reason why I haven’t used it extensively, so many moving parts.” The same applies to Prisma — it promises simplicity but delivers complexity.

The Generation Dance

Every time you change your schema, you have to run prisma generate. This regenerates thousands of lines of TypeScript code. Your IDE freezes. Your build process slows down. And heaven forbid you forget to run it – now your types are out of sync with your database.

Contrast this with Drizzle, where changes to your schema are immediately reflected because everything is just TypeScript. No code generation, no waiting, no extra steps.

The Migration Nightmare

Prisma’s migration system looks great in demos. In production, it’s a different story.

Data Loss by Design

Here’s something that actually happened to us: we renamed a column in our schema. Prisma’s migration system didn’t detect it as a rename — it treated it as dropping the old column and creating a new one.

All the data in that column was gone.

Drizzle handles this better. When it detects a possible renaming, it enters interactive mode and lets you choose your intention. Prisma? It just drops your data and generates a “helpful” warning about potential data loss.

The Black Box Problem

Prisma generates SQL migrations for you, but they’re often not what you’d write by hand. They’re verbose, sometimes inefficient, and hard to review. When you need to customize a migration, you’re fighting against the tool instead of working with it.

The Real Cost of Abstraction

Ted Neward called ORMs “the Vietnam War of computer science” back in 2008. His words were prophetic: “a quagmire which starts well, gets more complicated as time passes, and before long entraps its users in a commitment that has no clear demarcation point, no clear win conditions, and no clear exit strategy.”

ORMs promise to eliminate the “impedance mismatch” between object-oriented code and relational databases. But here’s the thing: they don’t eliminate it. They hide it.

And when that abstraction leaks (which it always does), you’re worse off than when you started because now you need to understand both your ORM’s quirks AND the SQL it generates.

What Smart Developers Are Using Instead

After our Prisma experience, we evaluated alternatives. Here’s what we found:

Drizzle: The TypeScript-Native Approach

Drizzle calls itself “If you know SQL, you know Drizzle.” And it’s true:

const users = await db
.select()
.from(usersTable)
.where(like(usersTable.email, '%john%'))
.leftJoin(postsTable, eq(usersTable.id, postsTable.userId));

This is SQL, but with full TypeScript safety. No code generation. No binaries. No magic.

Performance? Drizzle is orders of magnitude faster than Prisma in benchmarks. Bundle size? 1.5MB vs Prisma’s 6.5MB.

Raw SQL with Type Safety

Tools like SQLx in Rust show us what’s possible: compile-time checked SQL queries with zero runtime overhead.

In TypeScript, libraries like Kysely provide similar benefits:

const users = await db
.selectFrom('users')
.leftJoin('posts', 'users.id', 'posts.userId')
.select(['users.name', 'posts.title'])
.where('users.email', 'like', '%john%')
.execute();

It’s still SQL, but with TypeScript safety and IDE support.

The “Raw SQL is Actually Better” Movement

More developers are returning to raw SQL. Why? Because:

  1. It’s more transparent — you know exactly what queries are running
  2. It’s more performant — no abstraction overhead
  3. It’s more portable — SQL knowledge transfers between projects
  4. It’s more secure — you’re aware of injection risks, making you more careful

As one developer put it: “I’d argue that a 20-line SQL query is way easier to understand than trying to figure out what some ORM method chain is gonna generate. Plus, SQL is standardized — any developer can read it.”

The Security Implications Nobody Talks About

ORMs are supposed to be more secure because they prevent SQL injection. But they introduce new security problems:

Mass Assignment Vulnerabilities

This Prisma code looks safe:

app.post('/users', async (req, res) => {
const user = await prisma.user.create({
data: req.body
});
});

But what if the request body includes a role: 'admin' field you didn’t expect? Congratulations, you just created an admin user.

With raw SQL, this pattern is much less likely because you explicitly specify which fields to update.

Excessive Database Permissions

ORMs often require broader database permissions to function. They need to query table schema information, access metadata, and perform reflection. This violates the principle of least privilege.

The Bundle Size Problem

In 2024, we care about bundle sizes. Frameworks like Astro and SvelteKit emphasize minimal JavaScript. But then we add 6.5MB ORMs to our backend.

For edge computing and serverless functions, every kilobyte matters. Cloudflare’s free plan has a 3MB limit. Prisma eats up more than half of that before you write a single line of business logic.

When ORMs Still Make Sense

I’m not saying ORMs are always wrong. There are cases where they shine:

Rapid Prototyping: When you need to get something working quickly and performance isn’t critical.

Junior Teams: If your team doesn’t have strong SQL skills, an ORM can provide guardrails.

Simple CRUD Applications: For basic create/read/update/delete operations without complex relationships.

Enterprise Requirements: Some organizations mandate ORMs for compliance or standardization reasons.

But for most production applications, the costs outweigh the benefits.

The Path Forward

If you’re thinking about moving away from ORMs, here’s a migration strategy:

1. Start with New Features

Don’t rewrite everything at once. Build new features with your chosen alternative (Drizzle, Kysely, or raw SQL) and see how it feels.

2. Identify Performance Bottlenecks

Use profiling to find your slowest database operations. These are good candidates for migration because you’ll see immediate improvements.

3. Migrate One Domain at a Time

Pick a bounded context in your application and migrate all its database access together. This maintains consistency within domains.

4. Invest in Tooling

Set up proper database migration tools, query builders, and type generation. The initial setup takes time, but you’ll be more productive in the long run.

The Bottom Line

Prisma isn’t inherently evil. It’s a well-engineered tool that solves real problems. But it also creates new problems that many teams don’t anticipate.

The question isn’t whether Prisma is good or bad. The question is whether the problems it solves are worth the problems it creates for YOUR application.

In our case, the answer was no. Moving away from Prisma reduced our serverless costs by 40%, improved our performance by 3x, and made our codebase more maintainable.

But more importantly, it forced us to understand our database better. We stopped treating it as a dumb storage layer and started leveraging its power. Our queries became more efficient. Our data models became cleaner. Our debugging sessions became shorter.

ORMs promised to make us more productive by hiding complexity. But as it turns out, the complexity was never hidden — it was just moved to a place where we couldn’t see it until it was too late.

Maybe it’s time to stop hiding from our databases and start embracing them.


元の記事を確認する

関連記事