Ionic MongoDB NestJS

Using MongoDB with Ionic and NestJS



·

In the previous few NestJS tutorials we have been discussing how to set up a basic REST API that we can interact with using an Ionic application. We have covered making both GET and POST requests to the backend, but so far we have just been using dummy data and placeholder code – our API doesn’t really do anything yet.

In this tutorial, we will be covering how to integrate MongoDB into a NestJS backend and how to use that to add and retrieve records from an Ionic application. We will be walking through an example where we POST data to the NestJS backend which will then be stored in a MongoDB database, and we will also make a GET request to the NestJS backend in order to retrieve data from the MongoDB database to display in our Ionic application.

If you are not already familiar with creating a NestJS server, or if you are unfamiliar with making GET and POST requests from an Ionic application to a NestJS server, I would recommend reading through the other tutorials in this series first:

  1. An Introduction to NestJS for Ionic Developers
  2. Using Providers and HTTP Requests in a NestJS Backend
  3. Sending Data with POST Requests to a NestJS Backend

We will be continuing on from the code in the last tutorial above. Although you do not have to complete the previous tutorials in order to do this one, if you want to follow along step-by-step it will help to have already completed the previous tutorials.

MongoDB and Mongoose

This tutorial is going to focus on covering how to integrate MongoDB with Ionic and NestJS more than explaining the concept of MongoDB in general. In short, MongoDB is a document based NoSQL database and Mongoose is a library that allows us to define objects/schemas that represent the types of data/documents we want to store. Mongoose provides methods that make it easier to create and retrieve the data we are working with (and it also does a lot more than just that). MongoDB is the database that stores the data, and Mongoose makes it easier to interact with that database.

If you would like more information on both of these technologies, this tutorial is a particularly good introduction.

1. Installing MongoDB and Mongoose

In order to work with MongoDB on your machine, you will need to have it installed. If you do not already have MongoDB installed on your machine, you can find information on how to do that here. I also released a video recently that covers installing MongoDB on macOS: Installing MongoDB with Homebrew on macOS.

Once you have installed MongoDB, you will need to make sure to open a separate terminal window and run the following command:

mongod

This will start the MongoDB daemon, meaning that the database will be running in the background on your computer and you will be able to interact with it. You will also need to install the mongoose package and the NestJS package associated with that by running the following command in your NestJS server project:

npm install --save @nestjs/mongoose mongoose

The NestJS Backend

First, we are going to work on our NestJS server. We will walk through setting up a connection to the database, creating a schema to represent the data we want to store, creating a service to handle adding and retrieving records, and setting up the appropriate routes in the controller.

1. Connecting to MongoDB

In order to connect to MongoDB in our NestJS server, we need to add the MongooseModule to our root module. We will use the forRoot method to supply the connection address, which is exactly the same as what we would use if we were just using the standard mongoose.connect method described here.

Modify src/app.module.ts to reflect the following:

import { Module, HttpModule } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    HttpModule,
    MongooseModule.forRoot('mongodb://localhost/mydb')
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}

As you can see, we supply mongodb://localhost/mydb to the MongooseModule which will set up a connection to the mydb MongoDB database on localhost. Keep in mind that in a production environment this connection address would be different.

If you have been following along with the previous tutorials you might notice that we have made some other changes to this module. Previously, we had included a QuotesService and a MessagesController in this module. We will be getting rid of the QuotesService as this was just an example for a previous tutorial. Instead of adding the MessagesController directly to the root module, we are going to give the messages functionality its own module that we will import into the root module later. Since the Messages functionality is becoming a little more complex now, it is going to be neater to organise the functionality into its own module. Even though that isn’t strictly required, it does allow for better code organisation.

2. Create a Message Schema

As I mentioned before, we can use Mongoose to define the type of data we want to store in the database. A “schema” represents the structure of the data that we want to store – if you are familiar with types and interfaces it is basically the same concept.

Create a file at src/messages/message.schema.ts and add the following:

import * as mongoose from 'mongoose';

export const MessageSchema = new mongoose.Schema({
    content: String,
    submittedBy: String
});

We create a new Schema with new mongooose.Schema and we supply the properties that we want that schema to contain along with the types for those properties. The type of data that we are adding to this schema is the same as what we have defined in the message.dto.ts file in the previous tutorials (this represents the Data Transfer Object (DTO) used to POST data to a NestJS server). It makes sense that these match because we intend to store the same data that we will POST to the NestJS server in the MongoDB database.

3. Create the Messages Module

We are going to create the Messages module now as we need this to set up a Model based on our MessageSchema which will allow us to interact with messages in our database. This module will set up all of the message related functionality that we need, and then we can import this single module into our main root module (rather than having to add all of these things individually to app.module.ts).

Create a file at src/messages/messages.module.ts and add the following:

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { MessagesController } from './messages.controller';
import { MessagesService } from './messages.service';
import { MessageSchema } from './message.schema';

@Module({
    imports: [MongooseModule.forFeature([{name: 'Message', schema: MessageSchema}])],
    controllers: [MessagesController],
    providers: [MessagesService]
})
export class MessagesModule {

}

We use MongooseModule.forFeature to set up our Message model that we will make use of in a moment – this is based on the MessageSchema that we just created. Also, notice that we are importing MessagesService and adding it as a provider – we haven’t created this yet but we will in the next step.

4. Create a Messages Service

Now we are going to create a messages service that will handle adding documents to the MongoDB database and retrieving them.

Create a file at src/messages/messages.service.ts and add the following:

import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { MessageDto } from './message.dto';
import { Message } from './message.interface';

@Injectable()
export class MessagesService {

    constructor(@InjectModel('Message') private messageModel: Model<Message>){

    }

    async createMessage(messageDto: MessageDto): Promise<Message>{
        const message = new this.messageModel(messageDto);
        return await message.save();
    }

    async getMessages(): Promise<Message[]>{
        return await this.messageModel.find().exec();
    }

    async getMessage(id): Promise<Message>{
        return await this.messageModel.findOne({_id: id});
    }

}

We are creating three different methods here:

  • A createMessage method that will add a new document to the database
  • A getMessages method that will return all message documents from the database
  • A getMessage method that will return one specific document from the database (based on its _id)

In the constructor we add @InjectModel('Message') which will inject our Message model (which we just set up in the messages module file) into this class. We will be able to use this model to create new messages and retrieve them from the database.

Our createMessage method accepts the messageDto which will POST from our Ionic application to the NestJS backend. It then creates a new message model using the data from this DTO, and then calls the save method which will add it to the MongoDB database. We are returning the result of the save operation which will allow us to see the document that was added.

The getMessages method we call the find method on the messages model which will return all message documents from the database (as an array).

The getMessage method will accept an id parameter, and then it will return one document from the database that matches the id that was supplied. MongoDB _id fields are generated automatically if you add a document to the database that does not contain an _id.

Before this will work, we will need to define an interface for our messages since we are using Message as a type in this service.

Create a file at src/messages/message.interface.ts and add the following:

export interface Message {
    content: string;
    submittedBy: string;
}

5. Create the Routes

Now we need to create the appropriate routes in the message controller.

Modify src/messages/messages.controller.ts to reflect the following:

import { Controller, Get, Post, Body, Param } from '@nestjs/common';

import { MessageDto } from './message.dto';
import { MessagesService } from './messages.service';

@Controller('messages')
export class MessagesController {

    constructor(private messagesService: MessagesService){

    }

    @Post()
    async createMessage(@Body() message: MessageDto){
        return await this.messagesService.createMessage(message);
    }

    @Get()
    async getMessages(){
        return await this.messagesService.getMessages();
    }

    @Get(':id')
    async getMessage(@Param('id') id: String){
        return await this.messagesService.getMessage(id);
    }

}

I’ve already covered the concepts used above in the previous tutorials, so if you aren’t familiar with what is happening here make sure to check those out.

6. Import the Messages Module

Finally, we just need to import our messages module into the root module for the application.

Modify src/app.module.ts to reflect the following:

import { Module, HttpModule } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MessagesModule } from './messages/messages.module';

@Module({
  imports: [
    HttpModule,
    MessagesModule,
    MongooseModule.forRoot('mongodb://localhost/mydb')
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}

As you can see above, we can set up all of that messages functionality we just created with one clean and simple import in our root module.

The Ionic Frontend

Now we just need to update our frontend to make use of the new backend functionality – fortunately, there isn’t much we need to do here. It doesn’t really have to be an Ionic/Angular application either, as we are just interacting with a REST API.

1. Modify the Messages Service

Our frontend also has a messages service to handle making the calls to the backend, we will need to update that.

Modify src/app/services/messages.service.ts to reflect the following:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'

@Injectable({
  providedIn: 'root'
})
export class MessagesService {

  constructor(private http: HttpClient) { 

  }

  createMessage(message): Observable<Object> {

    return this.http.post('http://localhost:3000/messages', {
      content: message.content,
      submittedBy: message.submittedBy
    });

  }

  getMessages(): Observable<Object> {
    return this.http.get('http://localhost:3000/messages');
  }

  getMessage(id): Observable<Object> {
    return this.http.get(`http://localhost:3000/messages/${id}`);
  }

}
  1. Trigger Calls to the API

Now we just need to make some calls to the API we created. We will just trigger these from the home page.

Modify src/app/home/home.page.ts to reflect the following:

import { Component, OnInit } from '@angular/core';
import { MessagesService } from '../services/messages.service';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {

  constructor(private messages: MessagesService){

  }

  ngOnInit(){

    let testMessage = {
      content: 'Hello!',
      submittedBy: 'Josh'
    };

    let testId = '5c04b73880159ab69b1e29a9'

    // Create a test message
    this.messages.createMessage(testMessage).subscribe((res) => {
      console.log("Create message: ", res);
    });

    // Retrieve all messages
    this.messages.getMessages().subscribe((res) => {
      console.log("All messages: ", res);
    });

    // Retrieve one specific message
    this.messages.getMessage(testId).subscribe((res) => {
      console.log("Specific message: ", res);
    });

  }

}

This will trigger three separate calls to each of the different routes we added to the server. In the case of retrieving a specific message, I am using a testId – in order for you to replicate this, you will first need to add a document to the database and then copy its _id as the testId here. The first time you serve this application in the browser you will be able to see the _id of one of the documents that are created.

To run this code you will need to:

  • Make sure that the MongoDB daemon is running by executing the mongod command in a separate terminal window
  • Make sure that your NestJS server is running by executing the npm run start command in another terminal window
  • Serve your Ionic application by executing the ionic serve command in another terminal window

When you run the application, you should see something like this output to the console:

MongoDB Console Output in an Ionic Application

You can see the document that was just created, all of the documents currently in the database, and the specific document that matches the testId (assuming that you have set up a testId that actually exists).

Summary

The built-in support for MongoDB that NestJS provides makes for a really smooth experience if you want to use MongoDB as your database. You will be able to use all the features you would expect from Mongoose within an Angular style application architecture. We have only scratched the surface of what you might want to do with a MongoDB integration here, in future tutorials we are going to focus on more advanced concepts like adding authentication.

What to watch next...