Chapter 7. Mongoose

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.

A word about MongoDB

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.

A word about Mongoose

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.”).

Mongoose and Nest.js

Just like we saw in the TypeORM and the Sequelize chapters, Nest.js provides us with a module that we can use with Mongoose.

Getting started

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.

Set up the database

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:latest
    ports:
    - "27017:27017"
    volumes:
    - mongo_data:/data/db
  api:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - NODE_ENV=development
    depends_on:
      - mongo
    links:
      - mongo
    environment:
      PORT: 3000
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    command: >
      npm run start: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!

Start the containers

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.

Connect to the database

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(),
    ...
  ],
})
export class AppModule {}

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.

The connection string

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.)

The right argument for the forRoot() method

Now 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'),
    ...
  ],
})
export class AppModule {}

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.

Modelling our data

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.

Our first schema

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';

export const EntrySchema = new mongoose.Schema({
  _id: Schema.Types.ObjectId,
  title: String,
  body: String,
  image: String,
  created_at: Date,
});

The schema we just wrote is:

  1. Creating an object with the properties we need for our blog entries.
  2. Instantiating a new mongoose.Schema type object.
  3. Passing our object to the constructor of the mongoose.Schema type object.
  4. Exporting the instantiated 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.

Including the schema into the module

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 }]),
  ],
})
export class EntriesModule {}

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.

Include the new module into the main module

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,
    ...
  ],
})
export class AppModule {}

Using the schema

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.

The interface

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';

export interface Entry extends Document {
  readonly _id: string;
  readonly title: string;
  readonly body: string;
  readonly image: string;
  readonly created_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.

The service

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()
export class EntriesService {
  constructor(
    @InjectModel(EntrySchema) private readonly entryModel: Model<Entry>
  ) {}

  // this method retrieves all entries
  findAll() {
    return this.entryModel.find().exec();
  }

  // this method retrieves only one entry, by entry ID
  findById(id: string) {
    return this.entryModel.findById(id).exec();
  }

  // this method saves an entry in the database
  create(entry) {
    entry._id = new Types.ObjectId();
    const createdEntry = new this.entryModel(entry);
    return createdEntry.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 controller

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')
export class EntriesController {
  constructor(private readonly entriesSrv: EntriesService) {}

  @Get()
  findAll() {
    return this.entriesSrv.findAll();
  }

  @Get(':entryId')
  findById(@Param('entryId') entryId) {
    return this.entriesSrv.findById(entryId);
  }

  @Post()
  create(@Body() entry) {
    return this.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.

The first requests

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.

Relationships

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.)

Modelling relationships

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 * as mongoose from 'mongoose';

export const CommentSchema = new mongoose.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 * as mongoose from 'mongoose';

export const EntrySchema = new mongoose.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.

Saving relationships

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()
export class EntriesService {
  constructor(
    @InjectModel(EntrySchema) private readonly entryModel: Model<Entry>,
    @InjectModel(CommentSchema) private readonly commentModel: Model<Comment>
  ) {}

  // this method retrieves all entries
  findAll() {
    return this.entryModel.find().exec();
  }

  // this method retrieves only one entry, by entry ID
  findById(id: string) {
    return this.entryModel.findById(id).exec();
  }

  // this method saves an entry and a related comment in the database
  async create(input) {
    const { entry, comment } = input;

    // let's first take care of the entry (the owner of the relationship)
    entry._id = new Types.ObjectId();
    const entryToSave = new this.entryModel(entry);
    await entryToSave.save();

    // now we are ready to handle the comment
    // this is how we store in the comment the reference
    // to the entry it belongs to
    comment.entry = entryToSave._id;

    comment._id = new Types.ObjectId();
    const commentToSave = new this.commentModel(comment);
    commentToSave.save();

    return { success: true };
  }
}

The modified create() method is now:

  1. Assigning an ID to the entry.
  2. Saving the entry while assigning it to a const.
  3. Assigning an ID to the comment.
  4. Using the ID of the entry we created before as value of the entry property of the comment. This is the reference we mentioned before.
  5. Saving the comment.
  6. Returning a success status message.

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.

Reading relationships

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()
export class EntriesService {
  constructor(
    @InjectModel(EntrySchema) private readonly entryModel: Model<Entry>,
    @InjectModel(CommentSchema) private readonly commentModel: Model<Comment>
  ) {}

  // this method retrieves all entries
  findAll() {
    return this.entryModel.find().exec();
  }

  // this method retrieves only one entry, by entry ID,
  // including its related documents with the "comments" reference
  findById(id: string) {
    return this.entryModel
      .findById(id)
      .populate('comments')
      .exec();
  }

  // this method saves an entry and a related comment in the database
  async create(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.

Summary

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:

  • How to start up a local MongoDB instance with Docker Compose.
  • How to import the @nestjs/mongoose module in our root module and connect to our MongoDb instance.
  • What are schemas and how to create one for modelling our data.
  • Setting up a pipeline that allows us to write to and read from our MongoDB database as a reaction of requests made to our Nest.js endpoints.
  • How to establish relationships between different types of MongoDB documents and how to store and retrieve those relationships in an effective way.

In the next chapter we cover web sockets.