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
- 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
-
Document Lifecycle Management
- Atomic create/update/delete operations
- Built-in timestamp management (createdAt/updatedAt)
-
Validation Protocol
- Batch ObjectID verification via $in operator
- Type coercion safeguards for ID lists
-
Audit Capabilities
- Count method for aggregate analytics
- Full query logging for operational transparency
Enterprise Implementation Scenarios
-
User Management Systems
- Conflict detection during user registration
- Secure credential updates via atomic operations
-
Inventory Control Platforms
- Bulk product ID validation for order processing
- Optimized catalog browsing with lean queries
-
Financial Transaction Logs
- Immutable audit trails through createdAt sorting
- Atomic balance updates with rollback safeguards
Architectural Benefits
-
Cross-Cutting Concerns
- Centralized logging infrastructure
- Uniform error handling patterns
-
Database Agnosticism
- Abstracted Mongoose implementation details
- Clear separation between business logic and persistence
-
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.