DEV Community

Mofajjal Rasul
Mofajjal Rasul

Posted on • Edited on

3

A Generic Repository Pattern for NestJS with Mongoose for MongoDB

This analysis examines a sophisticated generic repository pattern for MongoDB integration in NestJS applications, demonstrating industry-standard practices for database abstraction, error mitigation, and type-safe query orchestration. The implementation adheres to SOLID principles while maintaining full compatibility with Mongoose's feature set.


Core Implementation

The complete unmodified repository class provides foundational CRUD operations:

import { ConflictException, Logger, NotFoundException } from "@nestjs/common";
import { ObjectId } from "mongodb";
import {
  Document,
  FilterQuery,
  FlattenMaps,
  Model,
  QueryOptions,
  SaveOptions,
  UpdateQuery,
  UpdateWithAggregationPipeline,
} from "mongoose";

export class GenericRepository<T extends Document> {
  private readonly internalLogger: Logger;
  private readonly internalModel: Model<T>;

  constructor(model: Model<T>, logger?: Logger) {
    this.internalModel = model;
    this.internalLogger = logger || new Logger(this.constructor.name);
  }

  async create(doc: Partial<T>, saveOptions: SaveOptions = {}): Promise<T> {
    try {
      const createdEntity = new this.internalModel(doc);
      const savedResult = await createdEntity.save(saveOptions);
      return savedResult;
    } catch (error) {
      if (error?.name === "MongoServerError" && error?.code === 11000) {
        this.internalLogger.error("Duplicate key error while creating:", error);
        throw new ConflictException("Document already exists with provided inputs");
      }
      throw error;
    }
  }

  async getAll(
    filter: FilterQuery<T> = {},
    options: QueryOptions = {},
  ): Promise<FlattenMaps<T>[]> {
    try {
      if (!options.sort) {
        options.sort = { createdAt: -1 };
      }
      const result = await this.internalModel
        .find(filter, null, options)
        .lean()
        .exec();
      return result;
    } catch (error) {
      this.internalLogger.error("Error finding entities:", error);
      return [];
    }
  }

  async getOneWhere(
    filter: FilterQuery<T>,
    options: QueryOptions = {},
  ): Promise<T | null> {
    try {
      const result = await this.internalModel
        .findOne(filter, null, options)
        .exec();
      return result;
    } catch (error) {
      this.internalLogger.error("Error finding entity by ID:", error);
      return null;
    }
  }

  async getOneById(id: string, options: QueryOptions = {}): Promise<T | null> {
    try {
      const result = await this.internalModel
        .findOne({ _id: id }, null, options)
        .exec();
      return result;
    } catch (error) {
      this.internalLogger.error("Error finding entity by ID:", error);
      return null;
    }
  }

  async updateOneById(
    documentId: string,
    updated: UpdateWithAggregationPipeline | UpdateQuery<T>,
    options: QueryOptions = {},
  ): Promise<T> {
    try {
      const result = await this.internalModel
        .findOneAndUpdate(
          { _id: documentId },
          { ...updated, updatedAt: new Date() },
          { ...options, new: true },
        )
        .exec();

      if (!result) {
        throw new NotFoundException("Document not found with provided ID");
      }
      return result;
    } catch (error) {
      if (error?.name === "MongoServerError" && error?.code === 11000) {
        this.internalLogger.error("Duplicate key error while updating:", error);
        throw new ConflictException("Document already exists with provided inputs");
      }
      this.internalLogger.error("Error updating one entity:", error);
      throw error;
    }
  }

  async removeOneById(id: string): Promise<boolean> {
    try {
      const { acknowledged } = await this.internalModel
        .deleteOne({ _id: id })
        .exec();
      return acknowledged;
    } catch (error) {
      this.internalLogger.error("Error removing entities:", error);
      throw error;
    }
  }

  async count(filter: FilterQuery<T> = {}): Promise<number> {
    try {
      const count = await this.internalModel.countDocuments(filter).exec();
      return count;
    } catch (error) {
      this.internalLogger.error("Error counting documents:", error);
      throw error;
    }
  }

  async validateObjectIds(listOfIds: string[] = []): Promise<boolean> {
    try {
      if (!Array.isArray(listOfIds) || !listOfIds?.length) {
        return false;
      }
      const objectIdStrings = listOfIds.map(String);
      const objectIds = objectIdStrings.map((id) => new ObjectId(id));

      const result = await this.internalModel
        .find({ _id: { $in: objectIds } })
        .select("_id")
        .lean()
        .exec();

      return listOfIds.length === result?.length;
    } catch (error) {
      this.internalLogger.error("Error during validation:", error);
      return false;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Structural Analysis

1. Type-Safe Foundation

  • Generic <T extends Document> parameter ensures collection-specific typing
  • FlattenMaps return type guarantees lean document serialization

2. Error Mitigation Strategy

  • Duplicate key detection (MongoServerError code 11000)
  • Contextual exception conversion (ConflictException/NotFoundException)
  • Fallthrough error propagation for custom handling

3. Query Optimization

  • Automatic createdAt sorting in getAll
  • Lean query execution for reduced memory footprint
  • Selective _id projection in validateObjectIds

Operational Paradigms

  1. Document Lifecycle Management

    • Atomic create/update/delete operations
    • Built-in timestamp management (createdAt/updatedAt)
  2. Validation Protocol

    • Batch ObjectID verification via $in operator
    • Type coercion safeguards for ID lists
  3. Audit Capabilities

    • Count method for aggregate analytics
    • Full query logging for operational transparency

Enterprise Implementation Scenarios

  1. User Management Systems

    • Conflict detection during user registration
    • Secure credential updates via atomic operations
  2. Inventory Control Platforms

    • Bulk product ID validation for order processing
    • Optimized catalog browsing with lean queries
  3. Financial Transaction Logs

    • Immutable audit trails through createdAt sorting
    • Atomic balance updates with rollback safeguards

Architectural Benefits

  1. Cross-Cutting Concerns

    • Centralized logging infrastructure
    • Uniform error handling patterns
  2. Database Agnosticism

    • Abstracted Mongoose implementation details
    • Clear separation between business logic and persistence
  3. Performance Considerations

    • Lean document returns minimize payload size
    • Batch operations reduce database roundtrips

This implementation establishes a robust foundation for enterprise-grade applications, providing:

  • Type Integrity: Compile-time validation of document structures
  • Operational Safety: Transaction-ready method signatures
  • Diagnostic Visibility: Contextual logging throughout data operations

The pattern serves as a blueprint for complex systems requiring maintainable, scalable data access layers while maintaining full compatibility with MongoDB's native capabilities.

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay