Lightweight Abstract Repository for NestJS
Josu Martinez
Posted on July 19, 2023
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;
}
}
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,
);
}
}
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);
}
}
}
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 {}
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.
Posted on July 19, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.