Alternative

Node.js series Part 3. The Simple Express Blog App with MongoDB

Date: 10.05.2020

Author: Patrick Rottländer

In Part 2 of my node.js series we have learned details about the most important functionalities of Express.js. We developed a small blog app that store data in a data file. With HTTP POST requests we can store data in the data file and with HTTP GET requests we can read data from the file and display data via the the response. In this Part 3 of my node.js series I extend the blog app with a MongoDB. So all of the application data are no longer stored in a data file but instead in a database. You will learn how data models and data controllers are implemented using Mongoose and some more interesting details how data operations can be defined.

This Article can be read in pdf format.

In this Part 3 of my node.js series I rebuild the simple blog app from the last Part 2 so that a MongoDB database is used instead of a simple data file. To do this, a MongoDB must first be installed locally on the development system. We also use Mongoose to access the database in our express blog application. I have described in detail on Digitaldocblog how to install MongoDB in my blog post Mongodb and Mongoose on Ubuntu Linux.

The creation of the database and the cloning of the application root directory from my GitHub page will be briefly described in the following step-by-step instructions. This is followed by a description of the application functionality. Then I will explain more generally how data is modeled in a database-based Expressjs application with the help of mongoose and how the data is accessed so that the more basic software architecture is understood. The relevant code of our blog app is commented. In this respect, the explanations of the individual code passages can be found in the inline documentation.

Create a MongoDB Database

After the installation of MongoDB you have started the mongod service on your system. It is very important that you setup MongoDB with client authentication enabled. You have also created an admin user with a password on your admin default db. Pls. follow the description in my article Mongodb and Mongoose on Ubuntu Linux.

Then you create a database for your project and a user for your database using the mongo command on the console.

Patricks-MBP:digitaldocblog-V3 patrick$ mongo

MongoDB shell version v4.2.3
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("4e8c592e-104d-48ef-b495-62129e446d23") }
MongoDB server version: 4.2.3

> db
test

> show dbs

> use admin
switched to db admin

> db.auth("admin", "YOUR-ADMIN-PASSWD")
1

> show dbs
admin               0.000GB
config              0.000GB
local               0.000GB

> use YOUR-DB-NAME
switched to db YOUR-DB-NAME

> db
YOUR-DB-NAME

> db.createCollection("col_default")
{ "ok" : 1 }

> show dbs
admin               0.000GB
config              0.000GB
local               0.000GB
YOUR-DB-NAME        0.000GB

> db.createUser({ user: "YOUR-DB-USER", pwd: "YOUR-DB-USER-PASSWD", roles: [{ role: "dbOwner", db: "YOUR-DB-NAME" }] })
Successfully added user: {
    "user" : "YOUR-DB-USER",
    "roles" : [
        {
            "role" : "dbOwner",
            "db" : "YOUR-DB-NAME"
        }
    ]
}

> show users
{
    "_id" : "YOUR-DB-NAME.YOUR-DB-USER",
    "userId" : UUID("ef080b98-849f-41f4-b561-a658a88e4595"),
    "user" : "YOUR-DB-USER",
    "db" : "YOUR-DB-NAME",
    "roles" : [
        {
            "role" : "dbOwner",
            "db" : "YOUR-DB-NAME"
        }
    ],
    "mechanisms" : [
        "SCRAM-SHA-1",
        "SCRAM-SHA-256"
    ]
}

> show dbs
admin               0.000GB
config              0.000GB
local               0.000GB
YOUR-DB-NAME        0.000GB
> 

> exit
bye

If you enter the command mongo on the console, the MongoDB client connects to the MongoDB service. The MongoDB client is the so-called MongoDB shell with which the user can work on the console. After the connection is established, the MongoDB shell is displayed in the terminal and with the command db we see the output test.

The db command shows the database you are currently connected to. test is the default database and is actually no longer used. The show dbs command shows all available databases. Since we have activated client authentication nothing is shown here because we are not yet authenticated.

With use admin we switch to admin db and authenticate ourselves with db.auth. After we have entered the db.auth command in the MongoDB shell as described, the shell shows us a 1. This shows that the authentication was successful. As admin user we can now show all databases with show dbs.

With the command use YOUR-DB-NAME we create the database YOUR-DB-NAME and also switch from the database admin to the database YOUR-DB-NAME. With the command db.createCollection I always create a default collection in YOUR-DB-NAME to display the new database YOUR-DB-NAME with the show dbs command in the MongoDB shell. If you simply create the database and no collection the new database is not shown with the show dbs command.

Then I create a user with db.createUser for my new database YOUR-DB-NAME, assign the role dbOwner to this user and the password YOUR-DB-USER-PASSWD.

The database YOUR-DB-NAME is now created and with exit we leave the MongoDB shell.

Create the application root directory

Clone the Simple Blog App from my GitHub page and then switch to the directory node-part-3-express-blog-with-db-V2.

Patricks-MBP:~ patrick$ cd node-part-3-express-blog-with-db-V2

The application root directory then looks like this.

Patricks-MBP:node-part-3-express-blog-with-db-V2 patrick$ ls -l
total 72
-rw-r--r--   1 patrick  staff    768 9 Apr 05:19 README.md
-rw-r--r--   1 patrick  staff   2162 9 Apr 05:19 blog.js
drwxr-xr-x   6 patrick  staff    192 9 Apr 05:19 database
drwxr-xr-x   3 patrick  staff     96 9 Apr 05:19 modules
drwxr-xr-x  77 patrick  staff   2464 9 Apr 05:19 node_modules
-rw-r--r--   1 patrick  staff  22302 9 Apr 05:19 package-lock.json
-rw-r--r--   1 patrick  staff    188 9 Apr 05:19 package.json

Patricks-MBP:node-part-3-express-blog-with-db-V2 patrick$ ls -l database
total 16
drwxr-xr-x  4 patrick  staff  128  9 Apr 05:19 controllers
-rw-r--r--  1 patrick  staff  704  9 Apr 05:19 db_.js
drwxr-xr-x  4 patrick  staff  128  9 Apr 05:19 models

Patricks-MBP:node-part-3-express-blog-with-db-V2 patrick$ ls -l database/controllers
total 40
-rw-r--r--  1 patrick  staff  9464 9 Apr 05:19 blogController.js
-rw-r--r--  1 patrick  staff  5306 9 Apr 05:19 userController.js

Patricks-MBP:node-part-3-express-blog-with-db-V2 patrick$ ls -l database/models
total 16
-rw-r--r--  1 patrick  staff  1574 9 Apr 05:19 blogModel.js
-rw-r--r--  1 patrick  staff  1216 9 Apr 05:19 userModel.js

Patricks-MBP:node-part-3-express-blog-with-db-V2 patrick$ ls -l modules
total 8
-rw-r--r--  1 patrick  staff  175  9 Apr 05:19 logger.js

Patricks-MBP:node-part-3-express-blog-with-db-V2 patrick$ 

Read the README.md. After you run npm install You must adapt your database connection string and rename database/db_.js into database/db.js. Your database connection string ist as follows.

mongodb://YOUR-DB-USER:YOUR-DB-USER-PASSWD@localhost:27017/YOUR-DB-NAME

The application should run with npm blog.js. The application code is explained and commented inline. In addition, I will explain details in the following chapters.

How does the application work ?

The application looks like a very simple web application but already contains a lot of program logic. This program logic is mainly contained in the two user and blog controllers.

The application data are users and blogs. These data are stored in a MongoDB and can be created, changed or deleted by the application. When data is entered, i.e. when data sets are created and when data fields are changed, input validation takes place. This input validation is based on the mongoose built in input validation and especially for string values on the mongoose custom validation, whereby it is checked whether the input of a string corresponds to a defined regular expression (regex for ECMA Java Script).

The primary identifier for users is the email address. The users must be unique and can only be created if the email address is not already assigned to an existing user.

The users can be blog authors and therefore they have a reference to each blogid whose author they are. Whenever a blog post is created, the application checks whether a user with the specified email address already exists and whether the email address already belongs to an existing user. If the user does not exist or the specified email address belongs to an existing user, the blog post cannot be created. If a user exists and it is also his own email address, the new blog post can be created and the author's name, first name and email address are stored on the blog in the database. In parallel the blogid of the new blog post is stored as reference on the user object.

We thus have a reference from users to their blogs where they are authors and a reference from blogs to the users or blog authors. When creating a blog, it is a condition that the author exists as a user in the database, but it may still be that we have blogs with authors in the database for which there is no longer a user. This is because it is possible to delete a user without deleting all blogs where the user is the author. Therefore it may still be that we have blogs with authors in the database for which there is no longer a user.

Consequently, when a blog is deleted, it is checked whether the author exists as a user. If the user no longer exists, the blog entry will still be deleted. If there is a user in the database, the blog post is deleted and the reference blogid on the user is deleted at the same time.

But we can also update the email address of a user. This is a very realistic use case because email addresses can change for users and consequently a user would like to change their changed email address accordingly in our application. Because the email address is the unique identifier of a user, we have to change the email address both on the user and on all blogs where the user is entered as the author.

Our application can also display blogs. All blogs are displayed or only the blogs for a specific year or month or only for a defined date. In order to display the blogs in this way, the blog data are selected accordingly from the database.

In addition, the application also displays a homepage and an about page, which have no further significance.

Data Models

To organize our application data in the MongoDB we define so called models using Mongoose. Models contain the essential data structure and data definitions that a database collection should have. Our database will have 2 collections one for the users and one for the blogs.

For each database collection exactly one model is defined in a seperate file. In our example, we need one model for each of the two collections colusers and colblogs.

For col_users the model is defined in the file

  • database/models/userModel.js

for the col_blogs the model is defined in the file

  • database/models/blogModel.js.

I would like to show a standard model here.

// model.js
....

mongoose = require('mongoose')
var Schema = mongoose.Schema

var dataSchema = new Schema ({ key1:..., key2:..., ..., keyN:...}) 

var Data = mongoose.model('col_data', dataSchema)

module.exports = Data

With mongoose.Schema we create a database Schema. With new Schema({…}) we define exactly what the Schema looks like and which keys or data fields are defined for each data object that will be stored in the collection. The new Schema is stored in a variable that typically describes the purpose of the new Schema (userSchema or blogSchema in our app). With mongoose.model() we create the model and map it to the database collection. The first argument in the mongoose.model() function is the singular name of the database collection. So if we create the model with coldata mongoose is looking for coldatas collection in the database. The second argument in the callback is the new Schema that we defined before. This model will be stored in the Variable Data and exported with module.exports.

It is extremely important that all input data are validated before the data is saved in the database. This ensures that the application only processes validated data.

In the model definition of or blog application, I mark all auto data fields with a preceding underscore. With auto data fields, the value is either created by the database (like the id) or a default value is always set (like created). This means that these data fields are never manipulated by data input.

Input data fields are subject to mongoose built-in validation. The topic of input validation is described in great detail in the mongoose documentation. Nevertheless, I would like to go into a few points of input validation here.

Basically the specification of validation properties takes place in the database Schema definition per key of the data object. We have built-in validation properties or short validators like required, minlength or maxlength but also the option to define a custom validation function using the property validate. Here in the example below the custom validation function of the key "name" checks the input for the match with a regex. If the input value does not correspond to the regex, the function return false and the validation has failed. If the function return true, the data can be saved and the validation was successful.

// model.js

....

mongoose = require('mongoose')
var Schema = mongoose.Schema

var dataSchema = new Schema ({ 

 // define the auto data fields
  _id: {
    type: Schema.ObjectId,
    auto: true
  },

  _created: {
    type: Date,
    default: Date.now()
  },

  // define the input data fields
  name: {
    type: String,
    required: true,
    // input string validation function
    // input sting must match regex criteria
    validate: function(name) {
      return /^[a-zA-Z0-9üÜäÄöÖ.’-]+$/.test(name)
    }
  },  
....

})

....

Data Controllers

To organize our application data accesses and data manipulations using Mongoose we define so called data controllers in separate files.

In principle, each data model that we have defined in our data model files are representing a data perimeter. A data perimeter can be accessed via so-called controller modules which are defined in separate controller files. These controller modules are in turn defined as controller functions in the corresponding data controller.

These controller functions define the data operations like search queries, create, update and delete data within the controller functions. These data operations are defined in a try {…} block and if an error occurs when accessing the data, this error is forwarded to the catch {…} block. This catch block then takes over the error handling. I do this try catch thing for all operations except for save(). With save() errors are handled directly in the callback.

In addition to the req and res parameter, the controller functions also receive next as a parameter. This is required to call the default error handler of the server which is defined in our main application file blog.js.

If the request cannot be ended with a response due to an error accessing the data, then the request will be passed to the catch block. The catch block call the next(error) function with the error as parameter in order to pass control to the default error handler to respond to the request.

If the request cannot be ended with a response due to an err accessing the data, then the request will also be passed to the catch block. But the catch block then call an individual error handler to respond to the request.

In our application we have the user data perimeter and the blog data perimeter and for each of these different data perimeters there is a separate data controller.

User Controller

The user data controller exists to control data access and data manipulations for the user data perimeter.

  • database/controllers/userController.js

The user data controller contain the following user controller modules and functions.

createUser

The createUser function create a new User object in the database. We try first to find a user with the given email address.

If an error occurs here in the query, the request error is passed to the default error handler via catch block. If no error occurs, either a user is found or not. If a user is found, the user cannot be created with createUser and the response is 400 bad request.

If no user is found, the user can be created. This means that the newUser object is saved and the input data is validated when it is saved. If an input data validation error occurs, we will respond with 400 bad request. otherwise there is a 200 ok response and the new user object has been successfully created in the database.

updateUserEmail

With the updateUserEmail function, the email address of a user object can be changed. In parallel also the author email will be adapted on the blog objects where the user is author of blogs.

First the user is searched for with his existing email address. If the function runs on an error, the request and the error are passed on to the default error handler with catch.

If no user is found, we respond with 400 bad request. If, on the other hand, a user is found, the email address can be updated by saving the user object with the new email address. When saving, the input validation takes place again. Only if the input validation was successful the storage of the user object is completed.

Now the email address must be updated on the blogs where the user is the author.

For this we are looking for all blogs where the author email corresponds to the existing email address of the user. If the search runs for an err, we will respond with an individual 502 bad gateway this time because the request has been partially processed up to this point. The email address on the user object has already been updated, but the data for updating the author email addresses on the blogs cannot be queried.

If there is no err in the blog query, the user may not be the author of any blog. Then we answer with 200 ok and confirm that only the email address on the user object has been changed.

If, on the other hand, the user is the author of blogs, we use the updateMany() function to update the email address of any blog found.

This updateMany() function receive a filter object with the previously existing email address of the user as first parameter and then the update object to replace this existing email address with the new email address of the user. This operation is executed on any blog found in the database and is also carried out in a try block. If an err occurs, the request is passed on for individual error handling to the catch block. Here in the catch block we answer with a 502 bad gateway because the update on the user object has been already performed successfully but the update of the author email address on the blogs found was not successful because the updateMany() operation failed.

If the blog update process was successful, we respond with 200 ok. In this case the email address has been changed on the user object and also on his blogs.

removeUser

With removeUser function a user object can be removed from the database. We try first to find the user that should be removed with the given email address. If an error occurs, the request error is passed to the default error handler via catch block.

If no error occurs, either a user is found or not.

If no user is found, the user cannot be removed and the response is 400 bad request.

If the user to be removed has been found we try to delete this user from the database using the deleteOne() function. If an error occurs here, the request error is also passed to the default error handler via catch block.

If the deletion was successful we respond with 200 ok.

Blog Controller

The blog data controller exists to control data access and manipulations for the blog data perimeter.

  • database/controllers/blogController.js

The blog data controller contain the following blog controller modules and functions.

createBlog

The createBlog module create a new blog object in the database and update the new blog id reference on the author user object.

The new blog object can only be created if the author data email, first- and lastname match with a user object in the database. So the author must already exist as a user in the database.

We try first to find a user with the author email address. If an error occurs, the request error is passed to the default error handler via catch block. If no error occurs, either a user is found or not.

If no user is found, we respond with 400 bad request.

If a user is found, it is checked whether the provided first name or last name does not match the first name or last name of the user in the database found with the given email address. If one of the names does not match, the condition is false and we send a 400 bad request.

If both names are the same, there is a perfect match of email, first name and last name and the provided author data are validated. Then the blog object can be saved and the blog input data will be validated.

If an input data validation error occurs, we will respond with 400 bad request.

In any other case the new blog has been saved to the database and we try to update the user object by adding the new blog id on the user object as a blog reference. Therefore, we try to use the updateOne() function to find the user object based on the author's email address to add the blog id of the newly created blog object as a blog reference.

This operation is carried out in a try block and if an err occurs, it is passed on to the catch block. In case of an err we answer with a 502 bad gateway. The blog object is created but the user object is still not updated because the updateOne() operation was not successful. If the updateOne() operation was successful we respond with 200 ok.

removeBlog

The removeBlog module remove a blog object from the database and update the user object by removing the relevant blog id reference on the author user object.

We try to find a blog object that should be removed based on the blog title. If an error occurs during that query the default error handling function will be invoked via the catch block and next(error). In case of no error the query return one blog object or null which means that no blog object has been found.

If no blog has been found we respond with 400 bad request.

If the query was successful and we found a blog object we try to find a user based on the author email address. If an error occurs the default error handling function will be invoked via the catch block. In case of no error the query return one user object or no user object.

If we have no user object found that mean that the author of the blog is no longer a user in our application. In this case we try to remove only the blog using the deleteOne() function. If this blog deletion function fail we have no success and an error. We catch this error in the catch block to invoke the default error handler function. If the deleteOne() function can be executed successfully, we respond with ok 200. The blog object with the title has been deleted.

If we have found a user object this means that this user is the author of the blog object we want to delete. In this case we first try to remove the blog object using the deleteOne() function and catch the error in the catch block to invoke the default error handler function when the execution of deleteOne() fail.

When the blog deletion was successful, we also try to delete the blog id as a reference on the user object using the updateOne() function. If updateOne throw an err we go ahead with catch and respond with 502 bad gateway as the blog could be removed but the user object update failed.

If the user update of the user object was successful we respond with 200 ok.

returnAllBlogs

The returnAllBlogs module is a function that searches all blog objects in the database and returns them in an array blogs when the requested url is /blogs. If the requested url is different than the next() function will be called to transfer to the next request handler in the row.

We try to search for all blog objects using the find() function. This find function return an error in case the query fail. In this case the catch block will invoke the default error handler function. In case the query was successful the function return an array blogs including the blog objects.

In case the array is empty or better the length is equal to 0 no blog object has been found in the database and we return 200 ok no blogs found.

In case there are blog objects available we create for each blog object found a dataset object of blog attributes containing the title, the author (including name, firstname and email) and the date, push this dataset into a blog array and return this blog array with 200 ok.

returnYearBlogs

The returnYearBlogs module is a function that searches blog objects of a certain year in the database and returns them in an array blogs when the requested url is /blogs/year. If the requested url is different than the next() function will be called to transfer to the next request handler in the row.

We try to search for all blog objects in a year using the find() function and return an error in case the query fail. In this case the catch block will invoke the default error handler function.

In case the query was successful the function return a query array blogs including the blog objects found for the relevant search criteria. In case no blog object has been found (length of blogs is equal to 0) in the database we return 200 ok no blogs found.

In case there are blog objects available we create for each blog object found a dataset object of blog attributes containing the title, the author (including name, firstname and email) and the date, push this dataset into another blog array and return this blog array with 200 ok.

returnMonthBlogs

The returnMonthBlogs module is a function that searches blog objects of a certain year and month in the database and returns them in an array blogs when the requested url is /blogs/year/month. If the requested url is different than the next() function will be called to transfer to the next request handler in the row.

We try to search for all blog objects in a year and month using the find() function and return an error in case the query fail. In this case the catch block will invoke the default error handler function.

In case the query was successful the function return a query array blogs including the blog objects found for the relevant search criteria. In case no blog object has been found (length blogs is equal to 0) in the database we return 200 ok no blogs found.

In case there are blog objects available we create for each blog object found a dataset object of blog attributes containing the title, the author (including name, firstname and email) and the date, push this dataset into another blog array and return this blog array with 200 ok.

returnDateBlogs

The returnDateBlogs module is a function that searches blog objects of a certain date in the database and returns them in an array blogs.

As this is the last routing handler in the app.get() routing function where the path is defined as /blogs/:year?/:month?/day? the requested url matches this definition or not.

In case the requested url is something like /blogs/year/month/day/else the url is not defined and then the default route error handler will be invoked and return 404 Not found.

In case the requested url path match /blogs/year/month/day we try to search for all blog objects for the date using the find() function and return an error in case the query fail. In case of an error the catch block will invoke the default error handler function.

In case the query was successful the function return a query array blogs including the blog objects found for the relevant search criteria.

In case no blog object has been found (length blogs is equal to 0) in the database we return 200 ok no blogs found.

In case there are blog objects available we create for each blog object found a dataset object of blog attributes containing the title, the author (including name, firstname and email) and the date, push this dataset into another blog array and return this blog array with 200 ok.

Data operations in detail

Our application perform the following data operations.

  • create data
  • search and return data
  • update data
  • delete data

These operations will be implemented in the controller modules. We use the following functions to perform these operations.

  • save()
  • find()
  • findOne()
  • updateOne()
  • updateMany()
  • deleteOne()

If we want to create a new Data object we call new Data( { … } ) and apply the key specifications according to the Schema defined in the model.

note: Please note that we are using new Data( { … } ) here because we export Data in the above model example definition. If you look in the model files of our blog app, you will notice that I export in the user model User and in the blog model Blog. Consequently, for example, when creating a new user object in the user data controller, the call new User( { … } ) is made.

The function save() receive a callback function with error as first parameter and data as second parameter. Within the callback we can work with error as the error object and with data as the data object that will be saved in the database. With save(), error handling takes place directly in the call back function using the if (error) {…} else {…} block.

// controller.js

newData = new Data({key1:..., key2:..., ..., keyN:...})

newData.save(function(error, data) {
    if (error) {

    // do something when an error occurs

    } else {

    // do what you need to do when the creation was successful

    } 
})

If we want to search and return data we call find() or findOne().

If we expect that a search return several data objects as a result we are using the find() function.

The find() function is covered with the try {…} catch {…} blog.

find() receive as first parameter a filter object as search criteria and as second parameter a callback function with error as first parameter and data as second parameter. Because we use try catch error handling is regulated in the catch block and we respond to the request in case of an error within this catch block.

Within the callback we can work with data which is an array containing all the data objects that have been found in the database. In case no data objects were found the data array is empty. In case the function fail we catch the error.

Suppose you want to search in Data perimeter for data objects that have a specificName. Then we pass the filter object { name: specificName } and then the callback function(error, data) {…} to the Data.find() function.

// controller.js

// create the filter object
var searchDataWithName = { name: specificName }

try {

    Data.find(searchDataWithName, function(error, data) {

    if (data.length == 0) {

    // do something when no data have been found 

    } else {

    // do something with the data array that have been found 

    }

    })

} catch (error) {

// do something when an error occurs

}

If a search should return only one data object as a result we are using the findOne() method.

The findOne() function is covered with the try {…} catch {…} blog.

findOne() receive as first parameter a filter object as search criteria and as second parameter a callback function with error as first parameter and data as second parameter. Because we use try catch error handling is regulated in the catch block and we respond to the request in case of an error within this catch block.

Within the callback we can work with data which is the object that has been found in the database. In case no data object has been found the value of data is null. In case the function fail we catch the error.

Suppose you want to search in Data perimeter for one data object that has a specificName. Then we pass the filter object { name: specificName } and then the callback function(error, data) {…} to the Data.findOne() function.

// controller.js


// create the filter object
var searchDataWithName = { name: specificName }

try {

Data.findOne(searchDataWithName, function(error, data) {

    if (!data) {

    // do something when no data have been found

    } else {

    // do something with the data in the data object that have been found 

    } 

    })

} catch (error) {

// do something when an error occurs

}

If we want to update an existing Data object we search the Data object, apply the changes and then call save(). We can also use updateOne() or updateMany().

If we do search and save() the data object first must be found in the database and will then be returned using the findOne() function. Then we can apply the changes to one or more data attribute values i.e. update the email address of a user and then call save() to save the updated data object back into the database. This procedure has the advantage that input validation is used with save() and is therefore recommended whenever we receive changes directly as input values and these input values must be validated.

// controller.js

// create the filter object
var searchDataWithName = { name: specificName }

try {

    Data.findOne(searchDataWithName, function(error, data) {

    if (!data) {

    // do something when no data have been found

    } else {

        // specify the update value
        var updateEmail = newEmail

        // update the value
        data.email = updateEmail

        data.save(function(err, updatedData) {

            if (err) {

            // do something when an err occurs

            } else {

            // do something when the update was successful

            }
        })
    } 
    })

} catch (error) {

// do something when an error occurs

}

If we use updateMany() or updateOne() the data objects or the one data object will be looked up and updated in the database in one step. The data object(s) will not be returned. This makes processing faster and saves network resources.

The functions updateMany() and updateOne() are both covered with the try {…} catch {…} blog.

updateMany() receive as first parameter a filter object as search criteria, as second parameter the update object and as third parameter a callback function with error as first parameter and result as second parameter. Because we use try catch error handling is regulated in the catch block and we respond to the request in case of an error within this catch block.

Within the callback we can work with result which is an object containing data about the processing result { n: 1, nModified: 1, ok: 1 }. This mean n objects found, n objects modified and the processing was ok (ok: 0 if the processing failed).

Suppose you want to find in Data perimeter for data objects that have a specificName and update all objects found with specificNewName. Then we pass the filter object { name: specificName }, the update object { name: specificNewName } and then the callback function(error, result) {…} to the Data.updateMany() function.

// controller.js

// create the filter object
var searchDataWithName = { name: specificName }

// create the update object
var updatedName = { name: specificNewName }

....

try {

   Data.updateMany(

     searchDataWithName, 

     updatedName, 

     function(err, result) {

       // do something with the result

     })

} catch (err) {

// do something with the err

}

....

updateOne() also receive as first parameter a filter object as search criteria, as second parameter the update object and as third parameter a callback function with error as first parameter and result as second parameter. Because we use try catch error handling is regulated in the catch block and we respond to the request in case of an error within this catch block.

Within the callback we can work with result which is an object containing data about the processing result { n: 1, nModified: 1, ok: 1 }. This mean n objects found, n objects modified and the processing was ok (ok: 0 if the processing failed).

Suppose you want to find in Data data perimeter for one data object that has a specificName and you want to update this single object with specificNewName. Then we pass the filter object { name: specificName }, the update object { name: specificNewName } and then the callback function(error, result) {…} to the Data.updateOne() function.

// controller.js

// create the filter object
var searchDataWithEmail = { name: specificEmail }

// create the update object
var updatedEmail = { name: specificNewEmail}

try {

   Data.updateOne(

     searchDataWithEmail, 

     updatedEmail, 

     function(err, result) {

     // do something with the result

    })

} catch (err) {

// do something with the err

}

....

If we want to remove an existing Data object we search for the data object using findOne() to get the data object returned, and then call deleteOne().

We do this 2 step approach i.e. in removeBlogs because we use the returned blog object to determine first the id of the blog object and then use it to find and update the user object. You can do this in one single step only by using deleteOne(), but take into account that you will not receive a data object with which further queries or updates can be carried out.

deleteOne() receive as first parameter a filter object as search criteria and as second parameter a callback function with error as first parameter and result as second parameter. Because we use try catch error handling is regulated in the catch block and we respond to the request in case of an error within this catch block.

Within the callback we can work with result which is an object containing data about the processing result { n: 1, deletedCount: 1, ok: 1 }. This mean n objects found, n objects deleted and the processing was ok (ok: 0 if the processing failed). You can use the result object in the callback to check if the object to be removed has been found or not. But this an operation we don't need to perform because we are using the 2 step approach with findOne() and then deleteOne().

....

if (result.n == 0) {
  // do something if no object to be removed has been found
} else {
  // do something when the delete was successful
}

....


Suppose you want to find in User data perimeter for a data object that has a specificName and you want to remove this object from the database. Then we pass the filter object { name: specificName } and then the callback function(error, result) {…} to the User.deleteOne() function.

// contoller.js

/ create the filter object
var searchDataWithName = { name: specificName }

try {

    User.findOne(searchDataWithName, function(error, user) {

    if (!user) {

    // do something when no data have been found

    } else {

        try {

        User.deleteOne(searchDataWithName, function(error, result) {

            // do something when the delete was successful

        })


        } catch (error) {

        // do something when an error occurs        
        }       
}
})


} catch (error) {

// do something when an error occurs

}

HTTP request Routing

In our application main file blog.js we define routes to process HTTP POST and HTTP GET requests.

HTTP POST requests are routed using the app.post('routingPath', routingHandler) method.

During the create-, update- and remove data operations app.post() routes are called and the request and response objects are transferred to the corresponding routingHandler to take over the request handling. The request body contain all relevant input data to process the request.

The routingHandler then calls the controller module and transfers the request and response object to this module. Then the module takes over the complete processing of the request and finally sends the response.

So for example if an HTTP POST request is made to the path /createusers the app.post('/createusers', userController.createUser) route is called and the createUser module take over the complete processing.

// blog.js

....

// load database controllers
const userController = require('./database/controllers/userController');
const blogController = require('./database/controllers/blogController');

....

// define routes (routing table)

....

app.post('/createusers', userController.createUser)

app.post('/updateuseremail', userController.updateUserEmail)

app.post('/removeusers', userController.removeUser)

app.post('/createblogs', blogController.createBlog)

app.post('/removeblogs', blogController.removeBlog)

....

You have to imagine it in reality with a real web application like this: On a website there is an HTML form and the end user of the application can enter user data such as last name, first name and email address to add for example a new user in the application or in the application database. The moment the user of the application clicks on the send button, the data entered in the form is transferred to the route /createusers with HTTP POST which means that the relevant app.post('/createusers', userController.createUser) function is called to handle this HTTP POST request.

Unfortunately we are not yet dealing with a real web application because there is no HTML form until now. This is exactly where Postman comes in to test an HTTP POST action. In principle, Postman replaces the form. With Postman I can (amongst other things) transfer HTTP POST requests to the application and thus I also transfer the request body. It is therefore necessary that you familiarize yourself with Postman to test the applications app.post() routes.

HTTP GET requests are routed using the app.get('routingPath', routingHandler) method.

So for example if an HTTP GET request is made to the path / the app.get('/', routingHandler) route is called and the routingHandler take over the complete processing and send directly the response. The same happens when an HTTP GET request is made on the route /about. This is easy and not spectacular.

// blog.js

....

// load database controllers
const userController = require('./database/controllers/userController');
const blogController = require('./database/controllers/blogController');

....

// define routes (routing table)

....

app.get('/', (req, res) => {
  res.send('Hello, this is the Blog Home Page.')
})

app.get('/about', (req, res) => {
  res.send('Hello, this is the Blog About Page.')
})

....

It is a bit more special when an HTTP GET request is made to the route app.get('/blogs/:year?/:month?/:day?', …. ).

The route definition here provides so-called request parameters. The /blogs route can be parameterized with the parameters year, month and day. The question mark behind each parameter indicates that the parameter is optional.

The request is forwarded to several routing handlers.

The first routing handler assigns the request url and the request parameters year, month and day to variables and calls next() to hand over the request to the next routing handler in the row.

The next routingHandler is blogController.returnAllBlogs. This routingHandler calls the controller module returnAllBlogs and transfers the request and response object to this module. Then the module takes over the complete processing of the request and finally sends an array containing all blogs as response if the request url is equal to /blogs otherwise next() is called to hand over the request to the next routing handler in the row.

The next routingHandler is blogController.returnYearBlogs. This routingHandler calls the controller module returnYearBlogs and transfers the request and response object to this module. Then the module takes over the complete processing of the request and finally sends an array containing all blogs in the specified year as response if the request url is equal to /blogs/year otherwise next() is called to hand over the request to the next routing handler in the row.

The next routingHandler is blogController.returnMonthBlogs. This routingHandler calls the controller module returnMonthBlogs and transfers the request and response object to this module. Then the module takes over the complete processing of the request and finally sends an array containing all blogs in the specified year and month as response if the request url is equal to /blogs/year/month otherwise next() is called to hand over the request to the next routing handler in the row.

The next and last routingHandler is blogController.returnDateBlogs. This routingHandler calls the controller module returnDateBlogs and transfers the request and response object to this module. Then the module takes over the complete processing of the request and finally sends an array containing all blogs from the specified date.

Any other route will end up in an error 404 Bad request.

// blog.js

....

// load database controllers
const userController = require('./database/controllers/userController');
const blogController = require('./database/controllers/blogController');

....

// define routes (routing table)

....

app.get('/blogs/:year?/:month?/:day?',

        (req, res, next) => {
          url = req.url
          year = req.params.year
          month = req.params.month
          day = req.params.day

          next()
        },

        blogController.returnAllBlogs,
        blogController.returnYearBlogs,
        blogController.returnMonthBlogs,
        blogController.returnDateBlogs

      )

....

Status Handling

In our application we work with the following HTTP Status codes.

  • code 200: status: Ok. Request completed.
  • code 500: status: Internal Server Error. Request failed due to server error.
  • code 400: status: Bad Request. Request failed due to wrong input data.
  • code 404: status: Not Found. Request failed due to wrong route.
  • code 502: status: Bad Gateway. Request partly completed but not fully completed due to missing data.

Summary and Outlook

In this part 3 of my node.js series I explained how to setup our express blog app using a MongoDB database. We learned how to create data models and controllers to control access to the data. But the blog app has still no HTML frontend or no HTML template rendering is used.

So in the next part of my node.js series I will introduce how to extend the blog app with an HTML interface. Therefore we implement the use of the template engine Pug. With Pug we can create an HTML interface for our blog application.