I have used all three in production. Prisma on Topo Contábil, a NestJS backend. TypeORM on two older services at a previous company. Raw SQL on performance-critical reporting queries. After years of using them, I have a clear preference.
Prisma wins, mostly
Prisma has the best developer experience of the three. You define your models in one schema file and get generated types, a query client, and migrations. The types follow the schema: if a User model has a required email, prisma.user.create without an email does not compile, and a query with select: { email: true } returns an object typed with only email, so reading user.name afterwards is a compile error. Auto-complete in your IDE works because the generated client knows every field, every relation, every possible query shape.
The migration system is solid. prisma migrate dev generates SQL migrations from schema changes. You can review them, edit them, and they are versioned in your repo. You stop wondering what the current state of the database should be.
Where Prisma struggles: complex queries. Anything involving subqueries, window functions, CTEs, or dynamic filtering gets ugly fast. The Prisma Client API was not designed for analytical queries and it shows.
I would skip TypeORM
I cannot recommend TypeORM for new projects. The decorator-based entity definitions look nice until you need to debug a type mismatch and realize the TypeScript types and the runtime behavior are two separate things. Nothing ties the nullable option to the TypeScript type of the property: you can declare name: string with nullable: true and the compiler will never tell you that name can come back null.
The query builder is powerful but the API is inconsistent. Some methods return the entity, some return a raw result, some return a wrapper object. You are constantly checking the docs to see which one you are dealing with.
TypeORM supports both Active Record and Data Mapper, and I do not think it handles either well. Pick one and half the examples online use the other.
The migration story is the worst part. Auto-generated migrations frequently produce incorrect SQL. I have seen it generate DROP COLUMN followed by ADD COLUMN instead of ALTER COLUMN on a large table.
Raw SQL when you need it
Sometimes you just need to write SQL. Prisma has prisma.$queryRaw for this. You lose type safety but you gain the full power of PostgreSQL: window functions, recursive CTEs, lateral joins, complex aggregations, all the stuff ORMs struggle with. Prisma's TypedSQL closes part of that gap: you write the query in a .sql file and Prisma generates a typed function for it.
On Topo Contábil, raw SQL is a minority of our queries, all of them reporting and analytics, where the Prisma Client would be too verbose or cannot express the query at all.
What I use
I use Prisma as the primary ORM, because I have not found a better developer experience, and prisma.$queryRaw when it cannot express what I need. I would avoid TypeORM on a new project. Drizzle and Kysely are also worth a look if you want a typed query builder closer to SQL; this comparison covers only the three above.