Best practices for applying the Repository pattern within a NestJS application

After reviewing the NestJS documentation and examining their sample source codes, it appears challenging to implement a Repository pattern between the service layer and the database layer (e.g. MongoDB).

In NestJS, database operations are executed directly within the Service class, such as in the CatsService class.

This approach presents several issues:

  1. Changing the type of database becomes complex due to its usage across different services
  2. The model (Document Mongo) is utilized in database schemas, service classes, controllers, and even in Resolvers if GraphQL is employed, resulting in the use of Mongo types in various layers which leads to TypeScript complications
  3. Mixing business logic with database handling logic within the service class (e.g. CatsService)
  4. And more...

Here's an example from the NestJS documentation showcasing the CatsService:

import { Model } from 'mongoose';
import { Injectable, Inject } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
import { CreateCatDto } from './dto/create-cat.dto';

@Injectable()
export class CatsService {
  constructor(
    @Inject('CAT_MODEL')
    private catModel: Model<Cat>,
  ) {}

  async create(createCatDto: CreateCatDto): Promise<Cat> {
    const createdCat = new this.catModel(createCatDto);
    return createdCat.save();
  }

  async findAll(): Promise<Cat[]> {
    return this.catModel.find().exec();
  }
}

The initial problem arises when importing the Model from mongoose in the CatsService:

import { Model } from 'mongoose';

Subsequently, another issue surfaces when returning the Model/Document to the client's layer (controller or resolver):

private catModel: Model<Cat>,
...
  async findAll(): Promise<Cat[]> {
    return this.catModel.find().exec();
  }

As a result of these challenges, the controller or resolver begins using MongoDB Models, creating tight coupling between them and the database along with its types:

import { Cat } from '../graphql.schema';

Furthermore, a complication arises when REST is used instead of GQL with TypeScript:

import { Customer } from './models/customer.model';

An error occurs in the following line:

Conversion of type 'import("c:/Test/src/customer/customer.schema").Customer' to type 'import("c:/Test/src/customer/models/customer.model"). Customer' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

const customer = await this.customerService.findById(id) as Customer;

This discrepancy stems from declaring the Business Model as the return type in the controller while the customerService class returns the MongoDB object.

import { Customer } from './models/customer.model';

Repository Pattern?

To address the aforementioned issues, considering the popular Repository Pattern could be a solution - right? I came across an article discussing the implementation of the repository pattern in NestJS with MongoDB: Implementing a Generic Repository Pattern Using NestJS

However, in my view, this Repository pattern implementation may not always be suitable for real-world scenarios and could be constrained by the drawbacks highlighted in the comments section of that article:

Provides some insights on implementing a generic Repository Pattern efficiently?

Hence, the outlined Repository pattern implementation may not fully align with the demands of a practical system.

Do you resonate with the concerns expressed in the mentioned comment? If so, how can we overcome the challenges to achieve:

  1. A proper implementation of the Repository pattern (especially in MongoDB)

  2. Clear separation of the controller/resolver layer from the service and database layers

  3. The ability to operate on nested documents by their ID, including updating, deleting, and inserting

Answer №1

Here is the code I have implemented:

Base repository structure:

export class CustomRepository<T> {
  repository: Model<T>;

  constructor(
    repository: Model<T, Record<string, never>, Record<string, never>>,
  ) {
    this.repository = repository;
  }

  async findData(filter: FilterQuery<T | any>, projection?: any): Promise<T[]> {
    return this.repository.find(filter, projection);
  }

  async findByIdentity(
    id: string,
    projection?: ProjectionType<T>,
    options?: QueryOptions<T>,
  ): Promise<T> {
    return this.repository.findById(id, projection, options);
  }

  async fetchOne(
    filter?: FilterQuery<T>,
    projection?: ProjectionType<T>,
    options?: QueryOptions<T>,
  ): Promise<T> {
    return this.repository.findOne(filter, projection, options);
  }

  async addNewEntry(doc: T | AnyObject | DocumentDefinition<T>): Promise<T> {
    return this.repository.create(doc);
  }
  .
  .
  .
}

User-specific repository design:

export default class ClientRepository extends CustomRepository<ClientDocument> {
  constructor(@InjectModel(Client.name) private clientModel: Model<ClientDocument>) {
    super(clientModel);
  }

  async findUserByEmail(email: string): Promise<ClientDocument> {
    return this.fetchOne({ email });
  }
}

Service section:

export default class CustomerService {
  constructor(private readonly clientRepository: ClientRepository) {}

  async registerCustomer(email: string, password: string): Promise<ClientDocument> {
    const user = await this.clientRepository.findUserByEmail(email);

    if (user) {
      throw new UserAlreadyRegisteredError();
    }

    const createdCustomer = await this.clientRepository.addNewEntry({ email, password });

    return createdCustomer;
  }
}

Main user module:

@Module({
  imports: [
    MongooseModule.forFeature([{ name: Client.name, schema: ClientSchema }]),
  ],
  controllers: [],
  providers: [ClientRepository, CustomerService],
  exports: [ClientRepository, CustomerService],
})
export default class ClientModule {}

MongoDB schema for users:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';

export type ClientDocument = HydratedDocument<Client>;

@Schema({
  versionKey: false,
  timestamps: true,
  toJSON: {
    virtuals: true,
    transform: (doc, ret) => {
      delete ret._id;
    },
  },
})
export class Client {
  @Prop({
    type: String,
    required: true,
  })
  email: string;

  @Prop({
    type: String,
    required: true,
  })
  password: string;
}

export const ClientSchema = SchemaFactory.createForClass(Client);

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Performing Group By query in Java using Mongo DB's 3.x Driver

Interested in mastering the implementation of a group by query using the Mongo DB Java 3.x Driver? The goal is to group your collection based on usernames, and then sort the results by the count of results in descending order (DESC). Below is the shell qu ...

Unable to configure unit tests for Vue project using Typescript due to TypeError: Unable to destructure property `polyfills` of 'undefined' or 'null'

I've been working on adding unit tests for an existing Vue project that uses Typescript. I followed the guidelines provided by vue-test-utils for using Typescript, but when I ran the test, I encountered an error message stating: TypeError: Cannot d ...

Studio 3T chooses all elements within an array column

I have a database collection stored in Mongo with the following schema (simplified): { "_id" : ObjectId("55a94615a243a426db43d81e"), "property_name" : "My Main Property", "domain_list" : [ "mynumber1url.com", "mynumber2url.c ...

Guide on using automapper in typescript to map a complex object to a "Map" or "Record" interface

I have been utilizing the automapper-ts with typescript plugin for automatic mapping. Check it out here While it works smoothly for simple objects, I encountered issues when dealing with complex ones like: Record<string, any> or Map<string, Anoth ...

I find that the value is consistently undefined whenever I attempt to set it within a promise in Angular

Hi there, I've encountered an issue with my getData() function in accountService.ts. I'm attempting to fetch user data and user account data simultaneously using a zip promise. Although the resolve works correctly and I receive the accurate data, ...

What exactly is a NativeScript app: is it the experience users have on their mobile devices, or the product they download from the app store?

Currently, I am diving into the world of Angular and exploring how to develop Angular applications with TypeScript while working on a C# ASP.Net Core Web Api project as the server side component. My question is this - if I create a NativeScript app in add ...

The scale line on the OpenLayers map displays the same metrics twice, even when the zoom level is different

When using the Openlayers Map scale line in Metric units, a specific zoom rate may be repeated twice during the zoom event, even though the actual zoom-in resolution varies on the map. In the provided link, you can observe that the zoom rates of 5km and ...

The Lenis smooth scrolling feature (GSAP) is not functioning properly

I have encountered an issue with the smooth scrolling feature of gsap causing a delay on my website. This problem is only resolved when I manually go into the browser settings and disable smooth scrolling by navigating to chrome://flags/#smooth-scrolling ...

Error: Code cannot be executed because the variable "sel" has not been defined in the HTML element

Every time I try to click on the div, I encounter an error message stating 'Uncaught ReferenceError: sel is not defined at HTMLDivElement.onclick' I am currently developing with Angular 8 and this error keeps popping up. I have read through simil ...

What is the best way to convert the reader.result into a string?

Whenever I attempt to upload an image on Angular, I encounter an error with reader.result in the TypeScript file below. How can I resolve this issue? I even included console.log(image) in the onImagePicked function but it doesn't display anything in ...

NestJS API experiencing issues connecting to MongoDB due to empty index keys

My goal is to create an API with NestJS using TypeORM. Initially, I had set up the API to work with Postgres, but now I need to migrate it to MongoDB. After making the necessary changes, the connection is established successfully. However, I encounter an ...

Issues with NextJS detecting environmental variables

I recently encountered an issue with my NextJS app running on Next.js v12.2.5 where it appears to be ignoring the environment variables I've configured. To address this, I created a .env.local file with the following content: NEXT_PUBLIC_SERVER_URL=h ...

Is it possible to implement drag and drop functionality for uploading .ply, .stl, and .obj files in an angular application?

One problem I'm facing is uploading 3D models in angular, specifically files with the extensions .ply, .stl, and .obj. The ng2-upload plugin I'm currently using for drag'n'drop doesn't support these file types. When I upload a file ...

Steps for setting up an IMAP server over a couchdb/NoSQL database

I am currently seeking a simple, open source solution to back up and archive multiple remote IMAP email accounts on a per user basis. I want to sync each user's email accounts using a low-cost, scalable method that efficiently utilizes server resource ...

Find all objects in an array that have a date property greater than today's date and return them

I have an array of objects with a property called createdDate stored as a string. I need to filter out all objects where the createdDate is greater than or equal to today's date. How can this be achieved in typescript/javascript? notMyScrims: Sc ...

Progress Bar Modules

I am currently working on creating a customizable animated progress bar that can be utilized as follows: <bar [type]="'health'" [percentage]="'80'"></bar> It is functional up to the point where I need to adjust different p ...

Creating a TypeScript type or interface that represents an object with one of many keys or simply a string

I am tasked with creating an interface that can either be a string or an object with one of three specific keys. The function I have takes care of different errors and returns the appropriate message: export const determineError = (error: ServerAlerts): ...

Establishing a connection between MySQL database and an Ionic/Angular application

I have been working on an Ionic/Angular project and I'm facing difficulties in establishing a connection with my MySQL database (mariadb). Despite trying various solutions from online sources, I keep encountering numerous error messages. Any guidance ...

The database is causing Mongod to crash during queries

My AWS instance is set up with nodejs and mongod running. The collection I am attempting to query from contains around 289,248 documents. Below is the code snippet I am using to fetch my data: var collection = db.collection('my_collection'); co ...

Tips for creating an operation within a JSON document?

Today, I am attempting to customize the appearance of my audiobook list. However, when trying to add an aspectRatio key-value pair to each object in my JSON file, I encountered an error. https://i.stack.imgur.com/Qb3TX.png https://i.stack.imgur.com/qTkmx. ...