MongoDB Abstract Repository for Node
Tags: node, webdev, mongodb, opensource
Published: 2023-06-04 16:54:17

[`monguito`](https://www.npmjs.com/package/monguito) is a lightweight and type-safe library to seamlessly create custom database repositories for Node.js applications. It provides developers with some built-in CRUD operations, enabling them to focus in adding their own operations, and ensures a clean separation between persistence and domain logic. Despite being database technology agnostic by nature, it currently includes an implementation for MongoDB.

### Note

This article introduces `monguito`, but it does not include all of its details. You may find the full documentation and some usage examples at [this GitHub repository](https://github.com/Josuto/monguito), including an example of [how to use this library in a NestJS-based application](https://github.com/Josuto/monguito/tree/main/examples/nestjs-mongoose-book-manager).

## Why Build Yet Another Repository Library

Joining [ClimatePartner](https://www.climatepartner.com/) made me switch my gears and start to develop JS/TS-based web applications. My very first assignment was connecting a small backend NestJS app with a MongoDB database. We had big plans for our app and we soon realised that the domain model owned by my team was going to grow in complexity, and quickly. Furthermore, we saw that this was also the reality in other teams that wanted to use the same kind of solution.

All considered, we decided to incorporate a database access utility that implements the [Repository pattern](https://www.martinfowler.com/eaaCatalog/repository.html) to cleanly decouple persistence from domain logic while keeping query logic duplication to the bare minimum. We also wanted it to be of _abstract_ nature so that we could reuse the database access logic that is common to all custom repositories (i.e., CRUD operations over any persistent domain object). Finally, we needed it to implement the  [Polymorphic pattern](https://www.mongodb.com/developer/products/mongodb/polymorphic-pattern/) to support subtyping-based domain models.

So we started researching the state-of-the-art and discovered several libraries that could fit our requirements. The best candidates we were able to find were [Mongoose](https://mongoosejs.com/), [Typegoose](https://typegoose.github.io/typegoose/), and [TypeORM](https://typeorm.io/). Mongoose is a well-known Node.js library for MongoDB that implements the [Data Mapper pattern](https://martinfowler.com/eaaCatalog/dataMapper.html) and lets developers define schemas to constraint the data models associated with their domain objects. However, Mongoose works with concrete data models, which in a complex domain model scenario results in query logic duplication. Typegoose is a type-safe Mongoose wrapper that allows schema constraint declaration at domain object field level via JS decorators. Unfortunately, those very decorators leak persistence logic into the domain model. Besides, Typegoose also implements the Data Mapper pattern, thus sharing the same drawbacks of Mongoose. TypeORM, on another hand, implements the Repository pattern and provides some basic support for MongoDB. However, TypeORM [presents several limitations compared to Mongoose](https://eliezer.medium.com/typeorm-mongodb-review-8855903228b1).

Although none of the existing alternatives were exactly fitting our needs, we saw value on Mongoose's solid, well-documented, and complete set of features. On the top of that, we wanted the most lightweight solution possible. That's why we decided to create `monguito`.

## Cut to the Chase

Say we have a domain model that specifies `Book` as the supertype domain object and `PaperBook` and `AudioBook` as subtypes. Here is one possible definition for that domain model:

```typescript
class Book implements Entity {
  readonly id?: string;
  readonly title: string;
  readonly description: string;
  readonly isbn: string;

  constructor(book: {
    id?: string;
    title: string;
    description: string;
    isbn: string;
  }) {
    this.id = book.id;
    this.title = book.title;
    this.description = book.description;
    this.isbn = book.isbn;
  }
}

class PaperBook extends Book {
  readonly edition: number;

  constructor(paperBook: {
    id?: string;
    title: string;
    description: string;
    isbn: string;
    edition: number;
  }) {
    super(paperBook);
    this.edition = paperBook.edition;
  }
}

class AudioBook extends Book {
  readonly hostingPlatforms: string[];
  readonly format?: string;

  constructor(audioBook: {
    id?: string;
    title: string;
    description: string;
    isbn: string;
    hostingPlatforms: string[];
  }) {
    super(audioBook);
    this.hostingPlatforms = audioBook.hostingPlatforms;
  }
}
```

Now, you want to be able to persist and retrieve instances of `Book` and any of its subtypes from a MongoDB database. `MongooseBookRepository` is a custom repository that specifies `Book`-related database operations. Here is its definition:

```typescript
class MongooseBookRepository
  extends MongooseRepository<Book>
  implements BookRepository
{
  constructor() {
    super({
      Default: { type: Book, schema: BookSchema },
      PaperBook: { type: PaperBook, schema: PaperBookSchema },
      AudioBook: { type: AudioBook, schema: AudioBookSchema },
    });
  }

  async findByIsbn<T extends Book>(isbn: string): Promise<Optional<T>> {
    if (!isbn)
      throw new IllegalArgumentException('The given ISBN must be valid');
    return this.entityModel
      .findOne({ isbn: isbn })
      .exec()
      .then((book) => Optional.ofNullable(this.instantiateFrom(book) as T));
  }
}
```

Voilà! `MongooseBookRepository` inherits a series of CRUD database operations from `MongooseRepository` and adds its own i.e., `findByIsbn`. Now you can simply instantiate `MongooseBookRepository` and execute any of the database operations as follows:

```typescript
const bookRepository = new MongooseBookRepository();
const books: Book[] = bookRepository.findAll();
```

No more leaking of persistence logic into your domain/application logic!

### Some Important Implementation Details

You may be wondering about certain elements exposed in the previous example. Starting from the top part of the `MongooseBookRepository` definition, the first of these is `MongooseRepository<Book>`. This is one of the two main elements exposed at `monguito`: a generic template that implements several common CRUD database operations. Furthermore, this class also handles the creation of a Mongoose data model capable of handling subtyping-based book data objects. All this complexity is hidden from you; all you need to know is that you have `entityModel` at your disposal to implement your own database operations, as shown at `findByIsbn`.

The only extra effort required is that you define a map that specifies the types that compose your domain model and their related Mongoose [schemas](https://mongoosejs.com/docs/guide.html), as specified at the constructor of `MongooseBookRepository`. It is also important to note that the key of the entry that specifies your domain object supertype must be named `Default`, and the key of each other entry must be named after the type name of the subtype domain object it refers to. This is a naming convention required by the inner implementation details of `MongooseRepository`. If you do not have any domain object subtype in your domain model then simply instantiate a map with a unique entry that specifies a `Default` key and the type-schema values of your domain object.

Another element that may catch your eye is `BookRepository`. This is an optional interface that extends `Repository`, the second main element exposed at `monguito`: a database technology agnostic generic interface that specifies all the supported CRUD database operations to be implemented by any repository. The following snippet shows this relation between these two interfaces:

```typescript
interface BookRepository extends Repository<Book> {
  findByIsbn: <T extends Book>(isbn: string) => Promise<Optional<T>>;
}
```

Handling instances of interface types instead of classes is a good practice in object-oriented applications; [depending on abstractions instead of implementations](https://en.wikipedia.org/wiki/Dependency_inversion_principle) makes your code scalable and better suited to change. Therefore, I would highly recommend that you define your own repository interfaces, or in the case you are only interested in exposing CRUD operations, instantiate your custom repositories as objects of type `Repository<T>`, where `T` is your domain object supertype.

On another hand, `Book` implements an interface called `Entity`. This interface models any persistent domain object type. The interface defines an optional `id` field. The optional nature of the field is due to the fact that its value is internally set by Mongoose. If you do not want your domain model to include this dependency, though, you can just ensure that your domain objects specify an `id?: string` field. And there is nothing wrong doing that, as `id` is the canonical primary key in MongoDB documents.

Finally, many database operations return objects of type [`Optional`](https://www.npmjs.com/package/typescript-optional). This type is inspired in the [Java `Optional` type](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Optional.html). Its purpose is to model the concept of "no result". This type is an excellent alternative to returning `null` objects or throwing an exception. The usage of `Optional` is safer and clearer than having to specify `T | null` as result type or [having to handle an exception that does not represent an application exceptional condition](https://github.com/EnosMatika/effective-java-3e/blob/master/10_exceptions/item_69_use_exceptions_only_for_exceptional_conditions.md). So yes, this the feature that makes `monguito` a bit opinionated, but I believe that is worth it.

## Conclusion

The `monguito` library provides the means to  create custom repositories for Node.js-based applications in a fast and easy manner. It is conceived to achieve effective decoupling between persistence and domain logic. Its current implementation is a Mongoose wrapper that includes all the boilerplate code required to perform common CRUD operations over polymorphic domain objects in MongoDB. Thus, developers can focus in writing domain-specific database operations at custom repositories.

Moreover, the library has been designed to allow developers use many interesting Mongoose features (e.g., creation and validation of polymorphic models as well as hook implementations, just two mention two) without having to deal with most of Mongoose's inner complexity.

Cover image by <a href="https://unsplash.com/@alevisionco?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Alev Takil</a> on <a href="https://unsplash.com/photos/d-1FY75fh_s?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>.