Lightweight Abstract Repository for NestJS
Tags: webdev, opensource, mongodb, nestjs
Published: 2023-07-19 16:00:00

In this article, I will explain how to develop a sample application using NestJS and MongoDB, leveraging the functionalities provided by the [`monguito`](https://www.npmjs.com/package/monguito), a lightweight and type-safe library to seamlessly create custom database repositories for Node.js applications.

The sample application is a book manager that includes features such as creating, updating, deleting, and finding books. I will also provide step-by-step instructions for installing and running the application.

For those eager to get to the code of the sample application, you can find it at [this Github repository](https://github.com/Josuto/monguito/tree/main/examples/nestjs-mongoose-book-manager). You may also find a further explanation on the `monguito` library at [this other article](https://dev.to/josuto/node-abstract-repository-for-mongodb-50jn).

## The Domain Model

The application domain model is pretty simple: `Book` is a supertype that specifies two subclasses i.e., `PaperBook` and `AudioBook`. Here is its definition:

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

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

export class PaperBook extends Book {
  readonly edition: number;

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

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

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

[`Entity`](https://github.com/Josuto/monguito/blob/main/src/util/entity.ts) is an interface created to assist developers in the implementation of type-safe domain models. It specifies an `id` field that all `Book` or subclass instances must include. This is because `id` is assumed to be the primary key of any stored book. However, you do not need to implement `Entity` if you do not want to; simply make sure that your superclass includes an `id` field in its definition.

## The Repository

Here a simple definition of a custom repository for books:

```typescript
@Injectable()
export class MongooseBookRepository
  extends MongooseRepository<Book>
  implements Repository<Book>
{
  constructor(@InjectConnection() connection: Connection) {
    super(
      {
        Default: { type: Book, schema: BookSchema },
        PaperBook: { type: PaperBook, schema: PaperBookSchema },
        AudioBook: { type: AudioBook, schema: AudioBookSchema },
      },
      connection,
    );
  }
}
```

[`Repository`](https://github.com/Josuto/monguito/blob/main/src/repository.ts) is a Typescript generic interface that specifies some common CRUD database operations that can be executed on any persistent domain object. [`MongooseRepository`](https://github.com/Josuto/monguito/blob/main/src/mongoose.repository.ts), on another hand, is a Mongoose-based implementation of the interface that provides you with all the boilerplate code required to execute those operations. You can find further details on both `Repository` and `MongooseRepository` in this [article](https://dev.to/josuto/node-abstract-repository-for-mongodb-50jn). I would personally recommend you read the section _Some Important Implementation Details_ to gain some required knowledge on the logic expected for your custom repository constructor, specially if your domain model is _polymorphic_ i.e., if you want to store instances of a type and/or its subtypes in the same MongoDB collection.

You may also have some further questions regarding the `Injectable` and `InjectConnection` decorators. Please be patient, I will address them soon enough in this article 😉.

## The Controller

The controller is the entry point of external requests to the book manager application. Here is how it looks like:

```typescript
type PartialBook = { id: string } & Partial<Book>;

function deserialise<T extends Book>(plainBook: any): T {
  let book = null;
  if (plainBook.edition) {
    book = new PaperBook(plainBook);
  } else if (plainBook.hostingPlatforms) {
    book = new AudioBook(plainBook);
  } else {
    book = new Book(plainBook);
  }
  return book;
}

@Controller('books')
export class BookController {
  constructor(
    @Inject('BOOK_REPOSITORY')
    private readonly bookRepository: Repository<Book>,
  ) {}

  @Get()
  async findAll(): Promise<Book[]> {
    return this.bookRepository.findAll();
  }

  @Post()
  async insert(
    @Body({
      transform: (plainBook) => deserialise(plainBook),
    })
    book: Book,
  ): Promise<Book> {
    return this.save(book);
  }

  @Patch()
  async update(
    @Body()
    book: PartialBook,
  ): Promise<Book> {
    return this.save(book);
  }

  @Delete(':id')
  async deleteById(@Param('id') id: string): Promise<boolean> {
    return this.bookRepository.deleteById(id);
  }

  private async save(book: Book | PartialBook): Promise<Book> {
    try {
      return await this.bookRepository.save(book);
    } catch (error) {
      throw new BadRequestException(error);
    }
  }
}
```

You may argue several things here. For starters, you may think that an enterprise application should delegate business/domain logic to a layer of service objects as described in e.g., [Domain-Driven Design (tactical design)](https://enterprisecraftsmanship.com/posts/domain-vs-application-services/). I have decided not to do so for simplicity purposes; the book manager presented in this article is such an extremely simple CRUD application that introducing services would be over-engineering. I rather implement the minimum amount of code necessary for the sake of maximising the actual purpose of the article: illustrate how to integrate the [`monguito`](https://www.npmjs.com/package/monguito) library on a NodeJS-based enterprise application.

Moreover, you would probably not write a `deserialise` function to enable the transformation of JSON request bodies into domain objects when dealing with `POST` requests. Instead, you would rather use a [NestJS pipe](https://docs.nestjs.com/pipes#pipes) to do so, thus properly complying with the Single Responsibility principle. Once again, I wanted to share the simplest possible working example at the expense of not conveying to the recommended practices in NestJS application construction. That being said, I would highly recommend you to read [this section](https://docs.nestjs.com/pipes#class-validator) on how to use `class-validator` and `class-transformer` for the validation and deserialisation of JSON request bodies in the development of complex enterprise applications.

Okay! So all is missing now is how to tell the controller to use the custom book repository at runtime. This is where we are going next.

## The Application Module

NestJS implements the Dependency Inversion principle; developers specify their component dependencies and NestJS uses its built-in dependency injector to inject those dependencies during component instantiation. 

So, how do we specify the dependencies of the components that compose the book manager sample application? There are two easy steps that we need to take: The first step consists of writing some decorators in the `MongooseBookRepository` and `BookController` classes, as I already did in the code definition for both. The former class specifies that its instances are `Injectable` to other components. It also specifies that to instantiate a book repository, NestJS needs to inject a Mongoose connection. This is done with the `InjectConnection` decorator related to the `connection` constructor input parameter. 

On another hand, the definition of `BookController` specifies that, during instantiation, the controller consumes an instance of a `Repository<Book>` defined by the `BOOK_REPOSITORY` custom token. 

The definition of the custom token is part of the second step: writing the last required class: `AppModule`. The definition of this class is as follows:

```typescript
@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost:27016/book-repository'),
  ],
  providers: [
    {
      provide: 'BOOK_REPOSITORY',
      useClass: MongooseBookRepository,
    },
  ],
  controllers: [BookController],
})
export class AppModule {}
```

The `Module` decorator of this class specifies the Mongoose connection required to instantiate `MongooseBookRepository` at the `imports` property. It also determines that any component that has a dependency with the `provider` identified by the `BOOK_REPOSITORY` custom token is to get an instance of `MongooseBookRepository`. Finally, it determines that `BookController` is the sole controller for the book manager application.

## Running the Application

Once you have implemented the book manager in your own or cloned the `monguito` [GitHub project](https://github.com/Josuto/monguito), and assuming that you have NestJS installed in your local machine, there are two further prerequisites to satisfy before you can run the application.

You must install all the project dependencies specified at the `package.json` file. To do so, simply execute the `yarn install` command from the root folder of the project. If you have cloned the library project, that folder is `examples/nestjs-mongoose-book-manager`.

The second prerequisite is installing Docker Desktop (in case you have not done it yet) and launching it in your local machine. I wrote a [`docker-compose.yml` file](https://github.com/Josuto/monguito/blob/main/examples/nestjs-mongoose-book-manager/docker-compose.yml) to create a Docker container with an instance of the latest MongoDB version for your convenience, but you may create your own otherwise.

Then, all you need to do is run `yarn start:dev`. You will then be able to access all the endpoints specified at `BookController` via e.g., the `curl` command or Postman.

## Conclusion

After following the several steps required to build an easy web application that integrates [`monguito`](https://www.npmjs.com/package/monguito), I hope that you are now able to leverage your knowledge and build your own. If you are willing to create a complex application, I also hope I has been able to provide you with a bit of guidance. Otherwise, please leave me a comment and I will get back to you ASAP 😊.

Cover image by <a href="https://unsplash.com/@michaelsum1228?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Michael Sum</a> on <a href="https://unsplash.com/images/animals/cat?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>.