Lightweight Abstract Repository for NestJS

josuto

Josu Martinez

Posted on July 19, 2023

Lightweight Abstract Repository for NestJS

In this article, I will explain how to develop a sample application using NestJS and MongoDB, leveraging the functionalities provided by the 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. You may also find a further explanation on the monguito library at this other article.

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:

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Entity 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:

@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,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Repository is a Typescript generic interface that specifies some common CRUD database operations that can be executed on any persistent domain object. MongooseRepository, 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. 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:

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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). 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 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 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 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:

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

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, 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 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, 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 Michael Sum on Unsplash.

💖 💪 🙅 🚩
josuto
Josu Martinez

Posted on July 19, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related