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

Having difficulty grasping the significance of the data received from the API response

Currently, as I am working on my personal Portfolio for a Web Developer course, I have encountered an issue with correctly implementing my API to retrieve information from the database. Previously, I faced no problem when using a .json file, but now, I am ...

billboard.js: The 'axis.x.type' property is conflicting with different data types in this context

axis: { x: { type: "category" } }, An issue has arisen: The different types of 'axis.x.type' are not compatible with each other. The value of 'string' cannot be assigned to '"category" | &qu ...

Exploring the combination of MongoDB and NextJS: Easily identify corresponding data regardless of capitalization

This code aims to retrieve and display the latest data on Covid-19 related fatalities, recoveries, and critical cases worldwide. The search function is defined as: const search = (e) => { e.preventDefault() //to prevent page reload cons ...

Is there a way to define one type parameter directly and another type parameter implicitly?

I am currently utilizing a UI-library that offers an API for constructing tables with a structure similar to this: type Column<Record> = { keys: string | Array<string>; render: (prop: any, record: Record) => React.ReactNode; } The l ...

How can I detect if a control value has been changed in a FormGroup within Angular 2? Are there any specific properties to look

I am working with a FormGroup that contains 15 editable items, including textboxes and dropdowns. I am looking to identify whether the user has made any edits to these items. Is there a specific property or method I can use to check if the value of any i ...

Ensuring Email Validation in MERN Application

I'm looking for guidance on how to implement email verification during the signup process in MERN Stack. I would like to send an email containing a link that redirects users to a specific page. Below is my node.js code for sign up - can anyone advise ...

How do I delete an element from an array in a MongoDB database using Node.js?

When running this query in ROBO 3T, the code executes correctly. However, when attempting to run it in nodejs, the code is not functioning as expected. //schema model const mongoose = require('mongoose'); const imageSchema = mongoose.Schema({ ...

Unlocking the secret to accessing keys from an unidentified data type in TypeScript

The code snippet above will not compile due to an error with the email protection link. const foo: unknown = {bar: 'baz'} if (foo && typeof foo === 'object' && 'bar' in foo) { console.log(foo.bar) } An erro ...

Having trouble establishing a connection to MongoDB from Node.js

Currently, I am in the process of working on a small blockchain prototype to enhance my understanding before delving into a larger project. After downloading MongoDB, I successfully executed the "mongod" command through CMD. However, upon attempting to in ...

Issue with Component in Angular not functioning properly with proxy construct trap

Currently working with Angular 17, I have a straightforward decorator that wraps the target with Proxy and a basic Angular component: function proxyDecorator(target: any) { return new Proxy(target, { construct(target: any, argArray: any[], newTarget: ...

Top Recommendations: Comparing Standalone Components and Modules in Angular Version 14

I'm in need of some clarification on the most effective practices when it comes to utilizing standalone components and modules within Angular 14. With the introduction of standalone components as a new concept in Angular, I am seeking factual guidance ...

What is the reason for Google Chrome extension popup HTML automatically adding background.js and content.js files?

While using webpack 5 to bundle my Google Chrome extension, I encountered an issue with the output popup HTML. It seems to include references to background.js and content.js even though I did not specify these references anywhere in the configuration file. ...

What is the best way to initialize a discriminated union in TypeScript using a given type?

Looking at the discriminated union named MyUnion, the aim is to invoke a function called createMyUnionObject using one of the specified types within MyUnion. Additionally, a suitable value for the value must be provided with the correct type. type MyUnion ...

Use leaflet.js in next js to conceal the remainder of the map surrounding the country

I'm currently facing an issue and would appreciate some assistance. My objective is to display only the map of Cameroon while hiding the other maps. I am utilizing Leaflet in conjunction with Next.js to showcase the map. I came across a helpful page R ...

What is the correct way to write SVG markup within SVG tags in a React and NextJS environment?

I currently have a Svg component set up like this interface SvgIconProps { children: React.ReactNode; strokeWidth?: number; width?: number; height?: number; className?: string; } export const SvgIcon = ({ children, strokeWidth = 1, width = ...

The search operation Dog.find() in MongoDB is happening before the record creation operation Dog.create

Issues with mongodb Dog.find() and Dog.create() sequence I recently began learning mongodb through an online tutorial on Cloud9. While practicing basic queries like find() and create(), I encountered a puzzling problem. Despite adding the Dog.create() met ...

Webpack resolve.alias is not properly identified by Typescript

In the Webpack configuration, I have set up the following: usersAlias: path.resolve(__dirname, '../src/pages/users'), In my tsconfig.json, you can find: "baseUrl": ".", "paths": { "usersAlias/*": ["src/pages/users/*"], } This is how the cod ...

Tips for hiding a sidebar by clicking away from it in JavaScript

My angular application for small devices has a working sidebar toggling feature, but I want the sidebar to close or hide when clicking anywhere on the page (i.e body). .component.html <nav class="sidebar sidebar-offcanvas active" id="sid ...

Creating custom designs for Material UI components

Although not a major issue, there is something that bothers me. I am currently using react, typescript, and css modules along with . The problem arises when styling material ui components as I find myself needing to use !important quite frequently. Is th ...

What are the most optimal configurations for tsconfig.json in conjunction with node.js modules?

Presently, I have 2 files located in "./src": index.ts and setConfig.ts. Both of these files import 'fs' and 'path' as follows: const fs = require('fs'); const path = require('path'); ...and this is causing TypeScr ...