A Generic Repository Pattern for NestJS with Mongoose for MongoDB

Published on Nov 3, 2024 | Reading time: 3 min


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;
    }
  }
}

Structural Analysis

1. Type-Safe Foundation

2. Error Mitigation Strategy

3. Query Optimization


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:

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