MongoDB _ NestJS - A progressive Node.js framework
MongoDB _ NestJS - A progressive Node.js framework
Nest supports two methods for integrating with the MongoDB database. You can either use the built-in
TypeORM module described here, which has a connector for MongoDB, or use Mongoose, the most popular
MongoDB object modeling tool. In this chapter we'll describe the latter, using the dedicated @nestjs/mongoose
package.
Once the installation process is complete, we can import the MongooseModule into the root AppModule .
app.module.ts JS
@Module({
imports: [MongooseModule.forRoot('mongodb://localhost/nest')],
})
export class AppModule {}
The forRoot() method accepts the same configuration object as mongoose.connect() from the Mongoose
package, as described here.
Model injection
With Mongoose, everything is derived from a Schema. Each schema maps to a MongoDB collection and defines
the shape of the documents within that collection. Schemas are used to define Models. Models are responsible
for creating and reading documents from the underlying MongoDB database.
Schemas can be created with NestJS decorators, or with Mongoose itself manually. Using decorators to create
schemas greatly reduces boilerplate and improves overall code readability.
schemas/cat.schema.ts JS
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
@Schema()
export class Cat {
@Prop()
name: string;
@Prop()
age: number;
@Prop()
breed: string;
}
HINT
Note you can also generate a raw schema definition using the DefinitionsFactory class (from the
nestjs/mongoose ). This allows you to manually modify the schema definition generated based on the
metadata you provided. This is useful for certain edge-cases where it may be hard to represent everything
with decorators.
The @Schema() decorator marks a class as a schema definition. It maps our Cat class to a MongoDB collection
of the same name, but with an additional “s” at the end - so the final mongo collection name will be cats . This
decorator accepts a single optional argument which is a schema options object. Think of it as the object you
would normally pass as a second argument of the mongoose.Schema class' constructor (e.g., new
mongoose.Schema(_, options) )). To learn more about available schema options, see this chapter.
The @Prop() decorator defines a property in the document. For example, in the schema definition above, we
defined three properties: name , age , and breed . The schema types for these properties are automatically
inferred thanks to TypeScript metadata (and reflection) capabilities. However, in more complex scenarios in
which types cannot be implicitly reflected (for example, arrays or nested object structures), types must be
indicated explicitly, as follows:
@Prop([String])
tags: string[];
Alternatively, the @Prop() decorator accepts an options object argument (read more about the available
options). With this, you can indicate whether a property is required or not, specify a default value, or mark it as
immutable. For example:
In case you want to specify relation to another model, later for populating, you can use @Prop() decorator as
well. For example, if Cat has Owner which is stored in a different collection called owners , the property
should have type and ref. For example:
In case there are multiple owners, your property configuration should look as follows:
Finally, the raw schema definition can also be passed to the decorator. This is useful when, for example, a
property represents a nested object which is not defined as a class. For this, use the raw() function from the
@nestjs/mongoose package, as follows:
@Prop(raw({
firstName: { type: String },
lastName: { type: String }
}))
details: Record<string, any>;
Alternatively, if you prefer not using decorators, you can define a schema manually. For example:
The cat.schema file resides in a folder in the cats directory, where we also define the CatsModule . While
you can store schema files wherever you prefer, we recommend storing them near their related domain objects,
in the appropriate module directory.
cats.module.ts JS
@Module({
imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
The MongooseModule provides the forFeature() method to configure the module, including defining which
models should be registered in the current scope. If you also want to use the models in another module, add
MongooseModule to the exports section of CatsModule and import CatsModule in the other module.
Once you've registered the schema, you can inject a Cat model into the CatsService using the
@InjectModel() decorator:
cats.service.ts JS
@Injectable()
export class CatsService {
constructor(@InjectModel(Cat.name) private catModel: Model<Cat>) {}
Connection
At times you may need to access the native Mongoose Connection object. For example, you may want to make
native API calls on the connection object. You can inject the Mongoose Connection by using the
@InjectConnection() decorator as follows:
@Injectable()
export class CatsService {
constructor(@InjectConnection() private connection: Connection) {}
}
Multiple databases
Some projects require multiple database connections. This can also be achieved with this module. To work with
multiple connections, first create the connections. In this case, connection naming becomes mandatory.
app.module.ts JS
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/test', {
connectionName: 'cats',
}),
MongooseModule.forRoot('mongodb://localhost/users', {
connectionName: 'users',
}),
],
})
export class AppModule {}
NOTICE
Please note that you shouldn't have multiple connections without a name, or with the same name,
otherwise they will get overridden.
With this setup, you have to tell the MongooseModule.forFeature() function which connection should be used.
@Module({
imports: [
MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }], 'cats'),
],
})
export class CatsModule {}
@Injectable()
export class CatsService {
constructor(@InjectConnection('cats') private connection: Connection) {}
}
To inject a given Connection to a custom provider (for example, factory provider), use the
getConnectionToken() function passing the name of the connection as an argument.
{
provide: CatsService,
useFactory: (catsConnection: Connection) => {
return new CatsService(catsConnection);
},
inject: [getConnectionToken('cats')],
}
If you are just looking to inject the model from a named database, you can use the connection name as a second
parameter to the @InjectModel() decorator.
cats.service.ts JS
@Injectable()
export class CatsService {
constructor(@InjectModel(Cat.name, 'cats') private catModel: Model<Cat>) {}
}
Hooks (middleware)
Middleware (also called pre and post hooks) are functions which are passed control during execution of
asynchronous functions. Middleware is specified on the schema level and is useful for writing plugins (source).
Calling pre() or post() after compiling a model does not work in Mongoose. To register a hook before model
registration, use the forFeatureAsync() method of the MongooseModule along with a factory provider (i.e.,
useFactory ). With this technique, you can access a schema object, then use the pre() or post() method to
register a hook on that schema. See example below:
@Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: Cat.name,
useFactory: () => {
const schema = CatsSchema;
schema.pre('save', function () {
console.log('Hello from pre save');
});
return schema;
},
},
]),
],
})
export class AppModule {}
Like other factory providers, our factory function can be async and can inject dependencies through inject .
@Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: Cat.name,
imports: [ConfigModule],
useFactory: (configService: ConfigService) => {
const schema = CatsSchema;
schema.pre('save', function() {
console.log(
`${configService.get('APP_NAME')}: Hello from pre save`,
),
});
return schema;
},
inject: [ConfigService],
},
]),
],
})
export class AppModule {}
Plugins
To register a plugin for a given schema, use the forFeatureAsync() method.
@Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: Cat.name,
useFactory: () => {
const schema = CatsSchema;
schema.plugin(require('mongoose-autopopulate'));
return schema;
},
},
]),
],
})
export class AppModule {}
To register a plugin for all schemas at once, call the .plugin() method of the Connection object. You should
access the connection before models are created; to do this, use the connectionFactory :
app.module.ts JS
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/test', {
connectionFactory: (connection) => {
connection.plugin(require('mongoose-autopopulate'));
return connection;
}
}),
],
})
export class AppModule {}
Discriminators
Discriminators are a schema inheritance mechanism. They enable you to have multiple models with
overlapping schemas on top of the same underlying MongoDB collection.
Suppose you wanted to track different types of events in a single collection. Every event will have a timestamp.
event.schema.ts JS
HINT
The way mongoose tells the difference between the different discriminator models is by the
"discriminator key", which is __t by default. Mongoose adds a String path called __t to your schemas
that it uses to track which discriminator this document is an instance of. You may also use the
discriminatorKey option to define the path for discrimination.
SignedUpEvent and ClickedLinkEvent instances will be stored in the same collection as generic events.
click-link-event.schema.ts JS
@Schema()
export class ClickedLinkEvent {
kind: string;
time: Date;
sign-up-event.schema.ts JS
@Schema()
export class SignUpEvent {
kind: string;
time: Date;
With this in place, use the discriminators option to register a discriminator for a given schema. It works on
both MongooseModule.forFeature and MongooseModule.forFeatureAsync :
event.module.ts JS
@Module({
imports: [
MongooseModule.forFeature([
{
name: Event.name,
schema: EventSchema,
discriminators: [
{ name: ClickedLinkEvent.name, schema: ClickedLinkEventSchema },
{ name: SignUpEvent.name, schema: SignUpEventSchema },
],
},
]),
]
})
export class EventsModule {}
export class EventsModule {}
Testing
When unit testing an application, we usually want to avoid any database connection, making our test suites
simpler to set up and faster to execute. But our classes might depend on models that are pulled from the
connection instance. How do we resolve these classes? The solution is to create mock models.
To make this easier, the @nestjs/mongoose package exposes a getModelToken() function that returns a
prepared injection token based on a token name. Using this token, you can easily provide a mock
implementation using any of the standard custom provider techniques, including useClass , useValue , and
useFactory . For example:
@Module({
providers: [
CatsService,
{
provide: getModelToken(Cat.name),
useValue: catModel,
},
],
})
export class CatsModule {}
In this example, a hardcoded catModel (object instance) will be provided whenever any consumer injects a
Model<Cat> using an @InjectModel() decorator.
Async configuration
When you need to pass module options asynchronously instead of statically, use the forRootAsync() method.
y p p y y y, y
As with most dynamic modules, Nest provides several techniques to deal with async configuration.
MongooseModule.forRootAsync({
useFactory: () => ({
uri: 'mongodb://localhost/nest',
}),
});
Like other factory providers, our factory function can be async and can inject dependencies through inject .
MongooseModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
uri: configService.get<string>('MONGODB_URI'),
}),
inject: [ConfigService],
});
Alternatively, you can configure the MongooseModule using a class instead of a factory, as shown below:
MongooseModule.forRootAsync({
useClass: MongooseConfigService,
});
The construction above instantiates MongooseConfigService inside MongooseModule , using it to create the
required options object. Note that in this example, the MongooseConfigService has to implement the
MongooseOptionsFactory interface, as shown below. The MongooseModule will call the
createMongooseOptions() method on the instantiated object of the supplied class.
@Injectable()
export class MongooseConfigService implements MongooseOptionsFactory {
createMongooseOptions(): MongooseModuleOptions {
return {
uri: 'mongodb://localhost/nest'
uri: mongodb://localhost/nest ,
};
}
}
If you want to reuse an existing options provider instead of creating a private copy inside the MongooseModule ,
use the useExisting syntax.
MongooseModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
Example
A working example is available here.
Support us
Nest is an MIT-licensed open source project. It can grow thanks to the support by these awesome people. If
you'd like to join them, please read more here.
Become a sponsor