Skip to main content

MongoDB Supercharged for Node

· 5 min read

Most of the times people choose NoSQL when they do not need to have the data stored relationally, most likely for storing things such as logs, sessions or any kind of data which needs high write speeds and easy scaling.

There's a common misconception, that in order to build an application that contains a lot of relations, a MongoDB would be a no-go. This is not only debunked in this article, but we will show you a superior way to handle your application with MongoDB.

MongoDB has evolved in the last 5 years, it is now ACID compliant, Supports Transactions, watching documents in real-time, database-level schema, and for those who don't know, it even supports JavaScript filtering code at database-level. We love it very much because it blends beautifully with our approach to have JS everywhere: on backend, frontend, mobile and database (in a way).

What we didn't love about this database, was the poor ORM (Object Relational Mapping) solutions or (ODM as they are called):

  • Mongoose has been designed for ES5 with old-fashioned god-model approach
  • Other solutions such as TypeORM, MikroORM while they offer ok tooling for SQL, they have very poor support for MongoDB, making their solution bloated and very slow.

Our goal was to design a modern, type-safe, async ORM specifically for MongoDB with the following criteria:

  • Type-safety everywhere
  • Ability to hook into events before/after, insert/update/delete
  • Behaviors for things such as: timestampable, soft-deletable, blameable,
  • Relational Superpowers
  • Migrations
  • Transactions

Thanks to Nova, we have a reliable and ultra-fast way of fetching related data that even beats RAW PostgreSQL by 2.5x (let alone ORMs that actually put your data into objects).

Get Started

You can integrate this solution in your existing application or benefit of a small application built by us.

git clone https://github.com/bluelibs/microframework-http-mongo my-projectcd my-projectnpm install# Assuming you have MongoDB on localhost:27017npm run start:dev
# Install MongoDBnpm i -g run-rsrun-rs --keep -d ~/mongodb-data

Typesafety Everywhere

Our data models aren't that smart (and that's a good thing), unlike mongoose in which your object is a god-model, which can save and persist data, in our case our objects are simply classes or Data Access Objects (DTOs) which contain information about their fields and optionally validation logic.

The whole concept of defining which indexes, links, reducers you've got are stored in the Collection class.

class Person {  constructor(data?: Partial<Person>) {    Object.assign(this, data);  }
  name: string;}
class PersonsCollection extends Collection<Person> {  static collectionName = "persons";}
const persons = container.get(PersonsCollection);
persons.insertOne(  new Person({    name: "John Smith",  })  // No-one forces you to use classes you can use interfaces and plain objects  // Classes are convenient because they can offer extra methods and ability to define validation via decorators);

Hooks

You can hook into before/after insert/update/delete using the beautiful async and type-safe event manager

postsCollection.on(BeforeInsertEvent, async (e) => {  // Access the document and enhance it however you wish.  e.data.document;});

Another approach would be to attach BeforeInsertEvent to the global event manager and add a filter on it, so that e.collection instanceof PostsCollection

All the event hooks are present:

  • BeforeInsertEvent
  • AfterInsertEvent
  • BeforeUpdateEvent
  • AfterUpdateEvent
  • BeforeDeleteEvent
  • AfterDeleteEvent

Read more here

Behaviors

Want to add timestampable or blameable behavior to your collections? Behaviors are functions that hook into events:

class PersonsCollection extends Collection<Person> {  static collectionName = "persons";
  static behaviors = [new Behaviors.Timestampable()];}

Read more here

Relational Data

Here's where things get interesting. We are leveraging Nova, the fastest relational data fetcher on Node that beats even PostgreSQL. We're around 4x times faster than mongoose due to the light-weight approach of models and better optimisation in general.

Any type of SQL-like relation can be emulated One-to-One, One-to-Many, Many-to-One and Many-to-Many. A key advantage for Many-to-Many is that you don't need intermediary tables, you can have an array of object ids: tagsIds: [ObjectId(), ObjectId()]

Query-ing data like a graph:

postsCollection.query({  title: 1,  name: 1,  tags: {    name: 1,  },  comments: {    text: 1,    author: {      name: 1,    },  },});

Persisting data, as an ORM, as you can see in our boilerplate you link data inside your objects and they get correctly persisted in the database.

The links are defined in the collection classes as such:

import { IBundleLinkOptions } from "@bluelibs/mongo-bundle";
class Posts extends Collection {  // ...  static links: IBundleLinkOptions = {    comments: {      collection: () => Posts,      inversedBy: "post",    },  };}
class Comments extends Collection {  static links: IBundleLinkOptions = {    post: {      collection: () => Posts,      field: "postId",    },  };}

Deep Sync (True ORM Experience)

Start working with MongoDB as you worked with an ORM:

const person = new Person({ ... });const comment = new Comment({ ... });person.comments.push(comment);
personsCollection.deepSync(person);
// person._id exists// person.comment exists

We have successfully added a way to synchronise class objects to a MongoDB in a relational way.

Read more here

Try it out!

All of the elements can be found inside the documentation here we just wanted to illustrate in few steps how advanced our ORM is, a quick and easy to use boilerplate.

The source-code for this package, can be found here, and while it is part of the BlueLibs ecosystem, it can definitely integrate in your existing application without too much hassle, as shown in the example repository.

Don't forget to star our GitHub repository if you enjoyed this article!