@nest-extended/mongoose

The Mongoose adapter: a generic CRUD service plus query helpers, ObjectId utilities and a MongoDB-aware exception filter.

$npm install @nest-extended/core @nest-extended/mongoose @nest-extended/decorators nestjs-cls

Key exports

ExportTypePurpose
NestService<M, D, E>classCRUD service — _find, _get, _create, _patch, _remove, and their event-firing find / get / create / patch / remove counterparts
nestify()functionApply $select / $populate / $sort / $limit / $skip to a query
rawQuery()functionConvert query params to a MongoDB filter (ObjectId + $regex aware)
EnsureObjectId()functionValidate / convert a string to an ObjectId
GlobalExceptionFilterfilterHandles HttpException, Mongoose, Zod and MongoServerError

Usage

cats.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { NestService } from '@nest-extended/mongoose';
import { Cat, CatDocument } from './schemas/cat.schema';
 
@Injectable()
export class CatsService extends NestService<Cat, CatDocument> {
  constructor(
    @InjectModel(Cat.name) private readonly catModel: Model<CatDocument>,
  ) {
    super(catModel);
  }
}

Keep the injected model on the instance (private readonly catModel), as the generator does. NestService holds its own private copy, so this is how you reach Mongoose directly — see Raw queries and aggregation.

Querying
await catsService.find({
  name: { $regex: 'kitty', $options: 'i' },
  age: { $gt: 5 },
  $populate: 'owner',
  $sort: { createdAt: -1 },
  $limit: 10,
});

Service events

Since 1.5.0 this service also exposes find / get / create / patch / remove — the same operations, but they dispatch to an attached events class. Name it as the last generic to have emit() type-checked:

export class CatsService extends NestService<Cat, CatDocument, CatsEvents> {}

See Service Events.

Raw queries and aggregation

find covers filtering, pagination and relations. For everything Mongoose can do beyond that — aggregation pipelines, distinct, bulkWrite, change streams, transactions — use the injected model directly. NestService never gets in the way.

cats.service.ts
@Injectable()
export class CatsService extends NestService<Cat, CatDocument> {
  constructor(
    @InjectModel(Cat.name) private readonly catModel: Model<CatDocument>,
  ) {
    super(catModel);
  }
 
  /** Average age per breed, with the owner joined in. */
  async statsByBreed() {
    return this.catModel.aggregate([
      { $match: { deleted: { $ne: true } } },
      {
        $lookup: {
          from: 'owners',
          localField: 'owner',
          foreignField: '_id',
          as: 'owner',
        },
      },
      { $unwind: { path: '$owner', preserveNullAndEmptyArrays: true } },
      {
        $group: {
          _id: '$breed',
          avgAge: { $avg: '$age' },
          total: { $sum: 1 },
          owners: { $addToSet: '$owner.name' },
        },
      },
      { $sort: { avgAge: -1 } },
    ]);
  }
 
  /** Anything else on the model works the same way. */
  async breeds() {
    return this.catModel.distinct('breed', { deleted: { $ne: true } });
  }
 
  async retireOldCats(age: number) {
    return this.catModel.bulkWrite([
      {
        updateMany: {
          filter: { age: { $gte: age }, deleted: { $ne: true } },
          update: { $set: { retired: true } },
        },
      },
    ]);
  }
}

Drop to the native driver with this.catModel.collection, and reach the connection — for transactions or db.command() — with this.catModel.db.

Three things the adapter stops doing

Raw access bypasses the wrapper entirely, so it is on you to:

  1. Filter soft-deleted documents. { deleted: { $ne: true } } is exactly what the adapter merges in for you — see Soft Delete & Auditing.
  2. Fire events. An aggregation is not find, so no hook runs. Call this.emit('statsByBreed', result) yourself if listeners should know — see Service Events.
  3. Shape the response. aggregate() returns a plain array, not the { total, $limit, $skip, data } envelope, and its documents are not hydrated Mongoose documents unless you ask for them.

Generated schemas set select: false on deleted, deletedAt, deletedBy and updatedBy. That is a query projection — aggregation pipelines ignore it, so those fields do appear in $project / $group output unless you exclude them.

Next steps