Photo by Yosuke Ota on Unsplash

On this page

This chapter follows Application lifecycle hooks in NestJS.

Mongoose already gives MongoDB schemas, validation, and models. What it does not give is a place in a Nest application: who opens the connection, how a service gets a model, and how a test replaces it. @nestjs/mongoose answers those with the patterns from the previous chapters. The connection is a forRootAsync() dynamic module, each feature registers its models with forFeature(), and models are injected by token. For Mongoose itself, schemas, queries, and population, see Mongoose with MongoDB.

Connecting

Terminal window
pnpm add @nestjs/mongoose mongoose

The connection is opened once, in the root module, with the URI from configuration:

src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
MongooseModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
uri: config.getOrThrow<string>('MONGODB_URI'),
}),
}),
CatsModule,
],
})
export class AppModule {}

Nest waits for the connection before it finishes starting, so a wrong URI fails at startup. The module also closes the connection during shutdown, which is one more reason to call app.enableShutdownHooks().

Defining a schema

Schemas can be written as classes with decorators. SchemaFactory turns the class into a regular Mongoose schema:

src/cats/schemas/cat.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Types } from 'mongoose';
@Schema({ timestamps: true })
export class Cat {
@Prop({ required: true, trim: true })
name: string;
@Prop({ min: 0 })
age: number;
@Prop({ index: true })
breed: string;
@Prop({ type: Types.ObjectId, ref: 'User', required: true })
owner: Types.ObjectId;
}
export type CatDocument = HydratedDocument<Cat>;
export const CatSchema = SchemaFactory.createForClass(Cat);
CatSchema.index({ owner: 1, name: 1 }, { unique: true });

The class is the type of a stored document. It is a separate thing from the CreateCatDto that validates a request: the DTO decides what a client may send, the schema decides what the database stores, and the two tend to differ (owner comes from the authenticated user, not from the body).

Registering and injecting a model

A feature module registers the models it uses on the shared connection:

src/cats/cats.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { Cat, CatSchema } from './schemas/cat.schema';
@Module({
imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}

The service injects the model with @InjectModel() and uses the normal Mongoose API:

src/cats/cats.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { isValidObjectId, Model } from 'mongoose';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './schemas/cat.schema';
@Injectable()
export class CatsService {
constructor(@InjectModel(Cat.name) private readonly catModel: Model<Cat>) {}
create(dto: CreateCatDto, ownerId: string) {
return this.catModel.create({ ...dto, owner: ownerId });
}
findAll(breed?: string) {
return this.catModel.find(breed ? { breed } : {}).lean().exec();
}
async findOne(id: string) {
const cat = isValidObjectId(id) ? await this.catModel.findById(id).lean().exec() : null;
if (!cat) throw new NotFoundException(`Cat ${id} not found`);
return cat;
}
}

lean() returns plain objects instead of full Mongoose documents. That is faster for reads that go straight into a response. Leave it off when the code needs document methods such as save().

Mapping database errors

A duplicate key violates the unique index and arrives as a Mongo error with code 11000. Left alone, the built-in filter answers 500. A small exception filter turns it into a 409:

src/common/mongo-duplicate-key.filter.ts
import { ArgumentsHost, Catch, ConflictException, ExceptionFilter } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { mongo } from 'mongoose';
@Catch(mongo.MongoServerError)
export class MongoDuplicateKeyFilter extends BaseExceptionFilter implements ExceptionFilter {
catch(exception: mongo.MongoServerError, host: ArgumentsHost) {
if (exception.code === 11000) {
return super.catch(new ConflictException('Resource already exists', { cause: exception }), host);
}
return super.catch(exception, host);
}
}

Extending BaseExceptionFilter hands the response back to Nest’s built-in filter, so the 409 has the same JSON shape as every other HTTP exception. When registered with APP_FILTER, BaseExceptionFilter receives the HTTP adapter through the injector, so the class needs no constructor of its own.

Testing with a model

@InjectModel(Cat.name) resolves to a provider whose token is getModelToken(Cat.name). A unit test supplies a fake under that token and never touches a database:

src/cats/cats.service.spec.ts
import { getModelToken } from '@nestjs/mongoose';
import { Test } from '@nestjs/testing';
import { CatsService } from './cats.service';
import { Cat } from './schemas/cat.schema';
describe('CatsService', () => {
const catModel = { find: jest.fn(), findById: jest.fn(), create: jest.fn() };
let service: CatsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [CatsService, { provide: getModelToken(Cat.name), useValue: catModel }],
}).compile();
service = moduleRef.get(CatsService);
});
});

For tests that should exercise real queries and indexes, mongodb-memory-server starts a throwaway MongoDB that forRootAsync() can point at. Testing NestJS applications covers both kinds of test.

Next: Authentication in NestJS.

Check your understanding

Question 1 of 5

0 of 5 checked

  1. 1. Why does the root module use MongooseModule.forRootAsync() rather than forRoot()?

Search articles

Type a word to search published articles.

Open the search page