Mongoose is the third and last database mapping tool that we will be covering in this book. It is the best known MongoDB mapping tool in the JavaScript world.
When MongoDB was initially released, in 2009, it took the database world by storm. At that point the vast majority of databases in use were relational, and MongoDB quickly grew to be the most popular non-relational database (also known as “NoSQL”.)
NoSQL databases difer from relational databases (such as MySQL, PostgreSQL, etc.) in that they model the data they store in ways other than tables related one to another.
MongoDB, specifically, is a “document-oriented database.” It saves data in “documents” encoded in BSON format (“Binary JSON”, a JSON extension that includes various data types specific for MongoDB). The MongoDB documents are grouped in “collections.”
Traditional relational databases separate data in tables and columns, similar to a spreadsheet. On the other hand, document-oriented databases store complete data objects in single instances of the database, similar to a text file.
While relational databases are heavily structured, document-oriented ones are much more flexible, since developers are free to use non-predefined structures in our documents, and even completely change our data structure from document instance to document instance.
This flexibility and lack of defined structure means that is usually easier and faster to “map” (transform) our objects in order to store them in the database. This brings reduced coding overhead and faster iterations to our projects.
Mongoose is technically not an ORM (Object Relational Mapping) though it’s commonly referred to as one. Rather, it is an ODM (Object Document Mapping) since MongoDB itself is based in documents instead of relational tables. The idea behind ODM’s and ORM’s is the same, though: providing an easy-to-use solution for data modelling.
Mongoose works with the notion of “schemas.” A schema is simply an object that defines a collection (a group of documents) and the properties and allowed types of values that the document instances will have (i.e. what we would call “their shape.”).
Just like we saw in the TypeORM and the Sequelize chapters, Nest.js provides us with a module that we can use with Mongoose.
As a first step, we need to install the Mongoose npm package, as well as the Nest.js/Mongoose npm package.
Run npm install --save mongoose @nestjs/mongoose in your console, and npm install --save-dev @types/mongoose inmediately after.
Docker Compose is the easiest way to get started with MongoDB. There’s an official MongoDB image in the Docker registry we recommend that you use. The latest stable version at the moment of writing this is 3.6.4.
Let’s create a Docker Compose file to build and start both the database we will be using, as well as our Nest.js app, and link them together so that we can access the database later from our code.
version:'3'volumes:mongo_data:services:mongo:image:mongo:latestports:-"27017:27017"volumes:-mongo_data:/data/dbapi:build:context:.dockerfile:Dockerfileargs:-NODE_ENV=developmentdepends_on:-mongolinks:-mongoenvironment:PORT:3000ports:-"3000:3000"volumes:-.:/app-/app/node_modulescommand:>npmrunstart:dev
We’re pointing to the latest tag of the MongoDB image, which is an alias that resolves to the most recent stable version. If you’re feeling adventurous, feel free to change the tag to unstable... though be aware that things might break!
Now that your Docker Compose file is ready, fire up the containers and start working!
Run docker-compose up in your console to do it.
Our local MongoDB instance is now running and ready to accept connections.
We need to import the Nest.js/Mongoose module that we installed a couple of steps ago into our main app module.
import{MongooseModule}from'@nestjs/mongoose';@Module({imports:[MongooseModule.forRoot(),...],})exportclassAppModule{}
We are adding the MongooseModule to the AppModule and we’re relying on the forRoot() method to properly inject the dependency. You might find the forRoot() method familiar if you read the chapter about TypeORM, or if you are familiar with Angular and its official routing module.
There’s a captcha with the code above: it won’t work, because there’s still no way for Mongoose or the MongooseModule to figure out how to connect to our MongoDB instance.
If you check in the Mongoose documentation or make a quick search on Google, you’ll see that the usual way of connecting to a MongoDB instance is by using the 'mongodb://localhost/test' string as an argument for the .connect() method in Mongoose (or even in the Node MongoDB native client.)
That string is what is known as a “connection string.” The connection string is what tells any MongoDB client how to connect to the corresponding MongoDB instance.
The bad news here is that, in our case, the “default” example connection string will not work, because we are running our database instance inside a container linked from another container, a Node.js one, which is the one that our code runs in.
The good news, though, is that we can use that Docker Compose link to connect to our database, because Docker Compose establishes a virtual network connection between both containers, the MongoDB one and the Node.js one.
So, the only thing that we need to do is changing the example connection string to
'mongodb://mongo:27017/nest'
where mongo is the name of our MongoDB container (we specified this is the Docker Compose file), 27017 is the port that the MongoDB container is exposing (27017 being the default for MongoDB), and nest is the collection we will store our documents on (you’re free to change it to your heart’s content.)
forRoot() methodNow that we have adjusted our connection string, let’s modify our original AppModule import.
import{MongooseModule}from'@nestjs/mongoose';@Module({imports:[MongooseModule.forRoot('mongodb://mongo:27017/nest'),...],})exportclassAppModule{}
The connection string is now added as an argument to the forRoot() method, so Mongoose is aware of how to connect to the database instance and will start successfully.
We already mentioned before that Mongoose works with the concept of “schemas.”
Mongoose schemas play a similar role to TypeORM entities. However, unlike the latter, the former are not classes, but rather plain objects that inherit from the Schema prototype defined (and exported) by Mongoose.
In any case, schemas need to be instantiated into “models” when you are ready to use them. We like to think about schemas as “blueprints” for objects, and about “models” as object factories.
With that said, let’s create our first entity, which we will name Entry. We will use this entity to store entries (posts) for our blog. We will create a new file at src/entries/entry.entity.ts; that way TypeORM will be able to find this entity file since earlier in our configuration we specified that entity files will follow the src/**/*.entity.ts file naming convention.
Let’s create our first schema. We will use it as a blueprint for storing our blog entries. We will also place the schema next to the other blog entries related files, grouping our files by “domain” (that is, by functionality.)
NOTE: You’re free to organize your schemas as you see fit. We (and the official Nest.js documentation) suggest storing them near the module where you use each one of them. In any case, you should be good with any other structural approach as long as you correctly import your schema files when you need them.
src/entries/entry.schema.ts
import{Schema}from'mongoose';exportconstEntrySchema=newmongoose.Schema({_id:Schema.Types.ObjectId,title:String,body:String,image:String,created_at:Date,});
The schema we just wrote is:
mongoose.Schema type object.mongoose.Schema type object.mongoose.Schema, so that it can be used elsewhere.NOTE: Storing the ID of our objects in a property called _id, starting with underscore, it’s a useful convention when working with Mongoose; it’ll make it possible later to rely on the Mongoose .findById() model method.
The next step is to “notify” the Nest.js MongooseModule that you intend to use the new schema we created. For that, we need to create an “Entry” module (in case we didn’t have one just yet) like the following:
src/entries/entries.module.ts
import{Module}from'@nestjs/common';import{MongooseModule}from'@nestjs/mongoose';import{EntrySchema}from'./entry.schema';@Module({imports:[MongooseModule.forFeature([{name:'Entry',schema:EntrySchema}]),],})exportclassEntriesModule{}
Quite similarly to what we did in the TypeORM chapter, we now need to use the forFeature() method of the MongooseModule in order to define the schemas that it needs to register to be used by models in the scope of the module.
Again, the approach is heavily influenced by Angular modules like the router, so it maybe looks familiar to you!
If not, note that this way of handling dependencies greatly increases the decoupling between functional modules in our apps, enabling us to easily include, remove and reuse features and functionality just by adding or removing modules to the imports in the main AppModule.
And, talking about the AppModule, don’t forget to import the new EntriesModule into the root AppModule, so that we can successfully use the new functionality we are writing for our blog. Let’s do it now!
import{MongooseModule}from'@nestjs/mongoose';import{EntriesModule}from'./entries/entries.module';@Module({imports:[MongooseModule.forRoot('mongodb://mongo:27017/nest'),EntriesModule,...],})exportclassAppModule{}
As mentioned before, we will use the schema we just defined to instantiate a new data model that we will be able to use in our code. Mongoose models are the ones that do the heavy lifting in regards to mapping objects to database documents, and also abstract common methods for operating with the data, such as .find() and .save().
If you’ve come from the TypeORM chapter, models in Mongoose are very similar to repositories in TypeORM.
When having to connect requests to data models, the typical approach in Nest.js is building dedicated services, which serve as the “touch point” with each model, and controllers. This links the services to the requests reaching the API. We follow the data model -> service -> controller approach in the following steps.
Before we create our service and controller, we need to write a small interface for our blog entries. This is because, as mentioned before, Mongoose schemas are not TypeScript classes, so in order to properly type the object to use it later, we will need to define a type for it first.
src/entries/entry.interface.ts
import{Document}from'mongoose';exportinterfaceEntryextendsDocument{readonly_id:string;readonlytitle:string;readonlybody:string;readonlyimage:string;readonlycreated_at:Date;}
Remember to keep your interface in sync with your schemas so that you don’t run into issues with the shape of your objects later.
Let’s create a service for our blog entries that interact with the Entry model.
src/entries/entries.service.ts
import{Component}from'@nestjs/common';import{InjectModel}from'@nestjs/mongoose';import{Model,Types}from'mongoose';import{EntrySchema}from'./entry.schema';import{Entry}from'./entry.interface';@Component()exportclassEntriesService{constructor(@InjectModel(EntrySchema)privatereadonlyentryModel:Model<Entry>){}// this method retrieves all entriesfindAll() {returnthis.entryModel.find().exec();}// this method retrieves only one entry, by entry IDfindById(id:string){returnthis.entryModel.findById(id).exec();}// this method saves an entry in the databasecreate(entry){entry._id=newTypes.ObjectId();constcreatedEntry=newthis.entryModel(entry);returncreatedEntry.save();}}
In the code above, the most important bit happens inside the constructor: we are using the @InjectModel() decorator to instantiate our model, by passing the desired schema (in this case, EntrySchema) as a decorator argument.
Then, in that same line of code, we are injecting the model as a dependency in the service, naming it as entryModel and assigning a Model type to it; from this point on, we can take advantage of all the goodies that Mongoose models offer to manipulate documents in an abstract, simplified way.
On the other hand, it’s worth mentioning that, in the create() method, we are adding an ID to the received entry object by using the _id property (as we previously defined on our schema) and generating a value using Mongoose’s built-in Types.ObjectId() method.
The last step we need to cover in the model -> service -> controller chain is the controller. The controller will make it possible to make an API request to the Nest.js app and either write to or read from the database.
This is how our controller should look like:
src/entries/entries.controller.ts
import{Controller,Get,Post,Body,Param}from'@nestjs/common';import{EntriesService}from'./entry.service';@Controller('entries')exportclassEntriesController{constructor(privatereadonlyentriesSrv:EntriesService){}@Get()findAll() {returnthis.entriesSrv.findAll();}@Get(':entryId')findById(@Param('entryId')entryId){returnthis.entriesSrv.findById(entryId);}@Post()create(@Body()entry){returnthis.entriesSrv.create(entry);}}
As usual, we are using Nest.js Dependency Injection to make the EntryService available in our EntryController. Then we route the three basic requests we are expecting to listen to (GET all entries, GET one entry by ID and POST a new entry) to the corresponding method in our service.
At this point, our Nest.js API is ready to listen to requests (both GET and POST) and operate on the data stored in our MongoDB instance based on those requests. In other words, we are ready to read from and write to our database from the API.
Let’s give it a try.
We will start with a GET request to the /entries endpoint. Obviously, since we haven’t created any entries yet, we should receive an empty array as a response.
> GET /entries HTTP/1.1 > Host: localhost:3000 < HTTP/1.1 200 OK []
Let’s create a new entry by sending a POST request to the entries endpoint and including in the request body a JSON object that matches the shape of our previously defined EntrySchema.
> GET /entries HTTP/1.1
> Host: localhost:3000
| {
| "title": "This is our first post",
| "body": "Bla bla bla bla bla",
| "image": "http://lorempixel.com/400",
| "created_at": "2018-04-15T17:42:13.911Z"
| }
< HTTP/1.1 201 Created
Yes! Our previous POST request triggered a write in the database. Let’s try to retrieve all entries once again.
> GET /entries HTTP/1.1
> Host: localhost:3000
< HTTP/1.1 200 OK
[{
"id": 1,
"title": "This is our first post",
"body": "Bla bla bla bla bla",
"image": "http://lorempixel.com/400",
"created_at": "2018-04-15T17:42:13.911Z"
}]
We just confirmed that requests to our /entries endpoint successfully execute reads and writes in our database. This means that our Nest.js app is usable now, since the basic functionality of almost any server application (that is, storing data and retrieving it on demand) is working properly.
While it’s true that MongoDB is not a relational database, it’s also true that it allows “join-like” operations for retrieving two (or more) related documents at once.
Fortunately for us, Mongoose includes a layer of abstraction for these operations and allows us to set up relationships between objects in a clear, concise way. This is provided by using refs in schemas’ properties, as well as the .populate() method (that triggers something known as the “population” process; more on it later.)
Let’s go back to our blog example. Remember that so far we only had a schema that defined our blog entries. We will create a second schema that will allow us to create comments for each blog entry, and save them to the database in a way that allows us later to retrieve both a blog entry as well as the comments that belong to it, all in a single database operation.
So, first, we create a CommentSchema like the following one:
src/comments/comment.schema.ts
import*asmongoosefrom'mongoose';exportconstCommentSchema=newmongoose.Schema({_id:Schema.Types.ObjectId,body:String,created_at:Date,entry:{type:Schema.Types.ObjectId,ref:'Entry'},});
This schema is, at this point, a “stripped-down version” of our previous EntrySchema. It’s actually dictated by the intended functionality, so we shouldn’t care too much about that fact.
Again, we are relying on the _id named property as a naming convention.
One notable new thing is the entry property. It will be used to store a reference to the entry each comment belongs to. The ref option is what tells Mongoose which model to use during population, which in our case is the Entry model. All _id’s we store here need to be document _id’s from the Entry model.
NOTE: We will ignore the Comment interface for brevity; it’s simple enough for you to be able to complete it on your own. Don’t forget to do it!
Second, we need to update our original EntrySchema in order to allow us to save references to the Comment instances that belong to each entry. See the following example on how to do it:
src/entries/entry.schema.ts
import*asmongoosefrom'mongoose';exportconstEntrySchema=newmongoose.Schema({_id:Schema.Types.ObjectId,title:String,body:String,image:String,created_at:Date,comments:[{type:Schema.Types.ObjectId,ref:'Comment'}],});
Note that the comments property that we just added is an array of objects, each of which have an ObjectId as well as a reference. The key here is including an array of related objects, as this array enables what we could call a “one-to-many” relationship, if we were in the context of a relational database.
In other words, each entry can have multiple comments, but each comment can only belong to one entry.
Once our relationship is modelled, we need to provide a method for saving them into our MongoDB instance.
When working with Mongoose, storing a model instance and its related instances requires some degree of manually nesting methods. Fortunately, async/await will make the task much easier.
Let’s modify our EntryService to save both the receive blog entry and a comment associated with it; both will be sent to the POST endpoint as different objects.
src/entries/entries.service.ts
import{Component}from'@nestjs/common';import{InjectModel}from'@nestjs/mongoose';import{Model,Types}from'mongoose';import{EntrySchema}from'./entry.schema';import{Entry}from'./entry.interface';import{CommentSchema}from'./comment.schema';import{Comment}from'./comment.interface';@Component()exportclassEntriesService{constructor(@InjectModel(EntrySchema)privatereadonlyentryModel:Model<Entry>,@InjectModel(CommentSchema)privatereadonlycommentModel:Model<Comment>){}// this method retrieves all entriesfindAll() {returnthis.entryModel.find().exec();}// this method retrieves only one entry, by entry IDfindById(id:string){returnthis.entryModel.findById(id).exec();}// this method saves an entry and a related comment in the databaseasynccreate(input){const{entry,comment}=input;// let's first take care of the entry (the owner of the relationship)entry._id=newTypes.ObjectId();constentryToSave=newthis.entryModel(entry);awaitentryToSave.save();// now we are ready to handle the comment// this is how we store in the comment the reference// to the entry it belongs tocomment.entry=entryToSave._id;comment._id=newTypes.ObjectId();constcommentToSave=newthis.commentModel(comment);commentToSave.save();return{success:true};}}
The modified create() method is now:
const.entry property of the comment. This is the reference we mentioned before.This way we make sure that, inside the comment, we are successfully storing a reference to the entry the comment belongs to. By the way, note that we store the reference by entry’s ID.
The next step should obviously be providing a way of reading from the database the related items we now are able to save to it.
As covered a few sections ago, the method that Mongoose provides for retrieving related documents from a database at once is called “population,” and it’s invoked with the built-in .populate() method.
We’ll see how to use this method by changing the EntryService once again; at this point, we will deal with the findById() method.
src/entries/entries.service.ts
import{Component}from'@nestjs/common';import{InjectModel}from'@nestjs/mongoose';import{Model,Types}from'mongoose';import{EntrySchema}from'./entry.schema';import{Entry}from'./entry.interface';import{CommentSchema}from'./comment.schema';import{Comment}from'./comment.interface';@Component()exportclassEntriesService{constructor(@InjectModel(EntrySchema)privatereadonlyentryModel:Model<Entry>,@InjectModel(CommentSchema)privatereadonlycommentModel:Model<Comment>){}// this method retrieves all entriesfindAll() {returnthis.entryModel.find().exec();}// this method retrieves only one entry, by entry ID,// including its related documents with the "comments" referencefindById(id:string){returnthis.entryModel.findById(id).populate('comments').exec();}// this method saves an entry and a related comment in the databaseasynccreate(input){...}}
The .populate('comments') method that we just included will transform the comments property value from an array of IDs to an array of actual documents that correspond with those IDs. In other words, their ID value is replaced with the Mongoose document returned from the database by performing a separate query before returning the results.
NoSQL databases are a powerful alternative to “traditional” relational ones. MongoDB is arguably the best known of the NoSQL databases in use today, and it works with documents encoded in a JSON variant. Using a document-based database such as MongoDB allows developers to use more flexible, loosely-structured data models and can improve iteration time in a fast-moving project.
The well known Mongoose library is an adaptor for MongoDB that works in Node.js and that abstracts quite a lot of complexity when it comes to querying and saving operations.
Over this chapter we’ve covered quite some aspects of working with Mongoose and Nest.js, like:
In the next chapter we cover web sockets.