Gebna

Stop Using Mongoose! Use Papr Instead

Written by Bahaa Zidan

Today I’m going to compare different ORMs / ODMs in terms of MongoDB support. Here’s a list of the packages we’re going to compare:

The comparison is going to consider the following aspects of each package:

Performance

When it comes to performance most available material focus on how fast a tool is in a certain benchmark. And while that can be important, it is usually more important that we look into the ways a certain tool give you in case of a performance bottleneck. Does the tool provide you with APIs to debug ? Does it provide you with an escape hatch in case its API couldn’t fulfill your use case ? Or does it just leave you in front of a tall concrete wall without a solution ?

Mongoose / TypeORM / MikroORM

Prisma

It saddens me to say that Prisma was the worst offender when it comes to performance. Their rust query engine seems to be very lacking with MongoDB. For example, when I was trying to fetch a single user using the username, the query took about 5 seconds!!! compared to ~150 ms with all other ORMs. Because for some reason (probably because of their built-in data-loader), the query engine translated one Prisma.findUnique call to a MongoDB.aggregate call instead of a MongoDB.findOne call. That was compounded by the fact that Prisma doesn’t support MongoDB collation natively which was needed for that particular query.

And while Prisma allow you to see what calls it’s making and also allow you to hit the database driver directly, if the query engine can’t figure out how to map a simple findOne call, I imagine we’re gonna be hitting that driver a lot if we go with Prisma.

Papr

Enters Papr. A paper-thin layer (sorry) on top of the MongoDB driver. It offloads the schema validation to MongoDB itself using native json schema validation instead of doing it in the application layer like Mongoose. It’s by far the closest performer to directly hitting the MongoDB driver.

Typesafety

Mongoose

There are 2 ways to add typesafety to Mongoose. The first is by writing an interface alongside your schema. Here’s how. The second way is by using Typegoose. Which is a package made to create the Mongoose schema and models using classes and decorators.

While Typegoose is more DRY because you write the types once. Both result in the same level of typesafety. Which is full of holes. For example, when you run User.create({name: 6}) it should fail because name is a string. It actually passes because for some reason Mongoose wraps every type you write in the interface/class with any 🙂.

TypeORM / MikroORM

Both ORMs provide a somewhat familiar way of representing our models. They’re both heavy on the OOP way of doing things. Where every model is presented as a class and the database jargon is then added by using a typescript decorator on each field. This approach is battle tested in other ecosystems, such as Hibernate, Doctrine and Entity Framework.

For typesafety, they both rely on reflection. Which means that our dev environment would need to constantly run the typescript compiler. I tried it with Vercel CLI hot reloading typescript environment and it was somewhat buggy.

MikroORM provides ts-morph as an alternative to reflect-metadata. But will need types to be shipped in the final bundle. Which is basically code generation with extra steps. Read Metadata Providers for more information.

And after going through all that hassle. The type-system of both of the ORMs isn’t foolproof. You’ll still be able to do things you shouldn’t be allowed to do and the compiler won’t complain.

Prisma

Prisma is 100% typesafe. And thanks to their introspection, you don’t even have to write the schema yourself. You execute a terminal command and Prisma will take a sample data from your database and analyze and write the schema for you. At least that’s the idea.

With MongoDB, Prisma is not able to introspect relations/references. Which means you’ll have to go through your generated schema file and add relations one by one manually. It can’t infer default values either. And it has a hard time dealing with the type ObjectId. Especially in arrays.

Schemas are written in Prisma’s own SDL. Which is very simple. But it adds to the complexity of the codebase. It’s worth noting that our schema file was more than 3000 lines of code. Prisma doesn’t support splitting or importing. All the community solutions for splitting are clunky at best.

After the schema is done, we generate a Prisma client. The client is a fully type-safe way to access the database. It is safe to say that Prisma has the best type-safety of all the previous solutions. Papr is the only one that came close.

Papr

Enter Papr. The alpha giga chad of MongoDB ODMs. It’s the only one of all of the above that is truly DRY. You only write the schema once. And it’s fully typesafe. The only time where the type system might yield unexpected results is when using the aggregation pipeline. Read this issue. It also doesn’t rely on reflection. So you can use any dev env you desire. It is truly the embodiment of the phrase “simple is powerful”.

Limitations

Typeorm / MikroORM

Prisma

Papr

Maturity + The Open Source Graveyard

In my opinion, whether a certain tool is ready for production or not depends on the following factors:

Mongoose:

TypeORM:

MikroORM:

It’s only maintained by a single developer. But the community around it is very impressive. It’s much smaller than Prisma or TypeORM though.

Prisma:

Papr:

Documentation

Most of the documentation I came across doing this research is generally good and usable. The only docs that left something to be desired is MikroORM. It felt like pages were taped together and there were a lot of adhoc paragraphs. It’s not ideal.

Conclusions