Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / Node.js

Creating Contact Manager App with MEAN Stack

4.96/5 (12 votes)
2 Oct 2017CPOL5 min read 39K  
Creating Contact Manager App with MEAN Stack

Introduction

MEAN Stack is a technology we can use to develop Web applications, MEAN is a stack composed of: MongoDB, ExpressJS, AngularJS and NodeJS. With MEAN stack, we have all components to develop a full application:

  • MongoDB: Database
  • ExpressJS: RESTful API
  • AngularJS: Presentation
  • NodeJS: JavaScript Server Side

Skills Prerequisites

  • JavaScript
  • Database concepts
  • Command Line

Software Prerequisites

  • NodeJS
  • MongoDB
  • Text Editor (VS Code, Vim, etc.)

Using the Code

Step 01 - Setup Environment

In order to work with this guide, we need to install NodeJS, so go to this link for downloading the latest version of NodeJS:. Once we have downloaded, run the installer, follow the steps and once you have installed, open a command line terminal and test the following command: node -v, that command shows NodeJS version, in my case shows me v5.9.1.

Next install MongoDB, download installer from MongoDB Download Center, install and follow the steps in wizard installation. In my case, the installation directory is: C:\Program Files\MongoDB\Server\3.2\bin, we can start the service for MongoDB with mongod, we'll get an output like this:

MongoDB Command Line

About ExpressJS and AngularJS, we can install them from command line with NPM, NPM is Node Package Manager, we can uninstall packages as well, for example if we want to install both packages, we can type in command line these commands:

  • npm install angular
  • npm install express

In case we type npm install, NPM will use package.json file to know which packages to install, if you have no idea how to write the content for package.json, just type npm init in command line and provide the information for your project.

Step 02 - Add Code

Once we have setup the environment, we'll create a contact manager app with MEAN Stack, we proceed to open a command line and follow these steps:

  1. Create a directory with name ContactManager.Mean
  2. Change to new directory
  3. Execute this command: npm init and provide information for your project
  4. Add body-parse package in package.json file
  5. Add express package in package.json file
  6. Add mongojs package in package.json file
  7. Save changes and restore packages

Our package.json file is this:

JavaScript
{
  "name": "contactmanager",
  "version": "1.0.0",
  "description": "Contact Manager App developed with MEAN Stack",
  "main": "server.js",
  "dependencies": {
    "body-parser": "^1.10.2",
    "express": "^4.11.1",
    "mongojs": "^0.18.1"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/hherzl/ContactManager.Mean.git"
  },
  "author": "C. Herzl",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/hherzl/ContactManager.Mean/issues"
  },
  "homepage": "https://github.com/hherzl/ContactManager.Mean#readme"
}

Now, we'll proceed to create a file with name Server.js with this code:

JavaScript
var express = require("express");
var bodyParser = require("body-parser");

var app = express();
app.use(express.static(__dirname + "/public"));
app.use(bodyParser.json());

var rest = require("./api");
var api = new rest(app);

var port = 3000;

app.listen(port);

console.log("Server running on port " + port);

api.js code file:

JavaScript
function api (app) {
    var mongojs = require("mongojs");
    
    var db = mongojs("contactManager", ["contacts"]);

    app.get("/api/contact", function (request, response) {
        var pageSize = request.query.pageSize ? parseInt(request.query.pageSize) : 10;
        var firstName = request.query.firstName;
        var middleName = request.query.middleName;
        var lastName = request.query.lastName;

        var find = {};

        if (firstName) {
            find.firstName = new RegExp(firstName, "i");
        }

        if (middleName) {
            find.middleName = new RegExp(middleName, "i");
        }

        if (lastName) {
            find.lastName = new RegExp(lastName, "i");
        }

        var fields = {
            firstName: 1,
            middleName: 1,
            lastName: 1,
            phone: 1,
            email: 1
        };

        var result = db.contacts.find(find, fields).limit
                     (pageSize, function (err, docs) {
            response.json(docs);
        });
    });

    app.get("/api/contact/:id", function (request, response) {
        var id = request.params.id;

        db.contacts.findOne({ _id: mongojs.ObjectId(id) }, function (err, doc) {
            if (err)
                console.log("Error: " + err);

            response.json(doc);
        });
    });

    app.post("/api/contact", function (request, response) {
        db.contacts.insert(request.body, function (err, doc) {
            if (err)
                console.log("Error: " + err);

            response.json(doc);
        });
    });

    app.put("/api/contact/:id", function (request, response) {
        var id = request.params.id;

        db.contacts.findAndModify({
            query: {
                _id: mongojs.ObjectId(id)
            },
            update: {
                $set: {
                    firstName: request.body.firstName,
                    middleName: request.body.middleName,
                    lastName: request.body.lastName,
                    phone: request.body.phone,
                    email: request.body.email
                }
            },
            new: true
        }, function (err, doc) {
            response.json(doc);
        });
    });

    app.delete("/api/contact/:id", function (request, response) {
        var id = request.params.id;

        db.contacts.remove({ _id: mongojs.ObjectId(id) }, function (err, doc) {
            if (err)
                console.log("Error: " + err);

            response.json(doc);
        });
    });
};

module.exports = api;

At the beginning of server.js file, we load all dependencies with require functions, next we enable static content with app.use(express.static(__dirname + "/public")); in public directory also, we set the body parser for all requests with this line: app.use(bodyParser.json());

Next, we add all API operations and as final, we set the listen port for app: app.listen(3000);

As we can see, in this file we have the ExpressJS framework to define API actions, for now we have the following URLs:

Verb URL Description
GET api/contact Retrieves all contacts
GET api/contact/id Retrieves a contact by id
POST api/contact Creates a new contact
PUT api/contact Updates an existing contact
DELETE api/contact Deletes an existing contact

All of those URLs are for API, inside of server.js we have the instance to access MongoDB database, the "table" contacts must to have the following columns:

  • firstName
  • middleName
  • lastName
  • phone
  • email

We can set the convention for API actions as we want, in my case, I prefer to use api prefix and use singular name to represent entities or features in API.

About AngularJS, we proceed to create the following directories:

  • controllers: Place for controllers
  • services: Place for services (reusable objects for controllers)
  • views: Place for HTML files

Now, we add a file with name app.js in root to define the AngularJS module:

JavaScript
var module = angular.module("contactManager", [
    "ngRoute",
    "ngAnimate",
    "toaster"
]);

(function (app) {
    app.config(function ($routeProvider) {
        var base = "/views/";

        $routeProvider
            .when("/", {
                templateUrl: base + "contact/index.html",
                controller: "HomeController",
                controllerAs: "vm"
            })
            .when("/contact/add", {
                templateUrl: base + "contact/add.html",
                controller: "ContactAddController",
                controllerAs: "vm"
            })
            .when("/contact/details/:id", {
                templateUrl: base + "contact/details.html",
                controller: "ContactDetailsController",
                controllerAs: "vm"
            })
            .when("/contact/edit/:id", {
                templateUrl: base + "contact/edit.html",
                controller: "ContactEditController",
                controllerAs: "vm"
            })
            .when("/contact/remove/:id", {
                templateUrl: base + "contact/remove.html",
                controller: "ContactRemoveController",
                controllerAs: "vm"
            });
    });
})(angular.module("contactManager"));

This file contains the definition of AngularJS module and route table, we are using the same naming convention for route table, singular for entities.

In services directory, we'll place all injectable services for module, for this case in this service, we'll add the code to provide all API related operations:

JavaScript
(function (app) {
    "use strict";

    app.service("RepositoryService", RepositoryService);

    RepositoryService.$inject = ["$log", "$http"];

    function RepositoryService($log, $http) {
        var svc = this;

        var apiUrl = "/api";

        svc.getContacts = getContacts;
        svc.getContact = getContact;
        svc.createContact = createContact;
        svc.updateContact = updateContact;
        svc.deleteContact = deleteContact;

        function getContacts(fields) {
            var queryString = [];

            if (fields.pageSize) {
                queryString.push("pageSize=" + fields.pageSize);
            }

            if (fields.firstName) {
                queryString.push("firstName=" + fields.firstName);
            }

            if (fields.middleName) {
                queryString.push("middleName=" + fields.middleName);
            }

            if (fields.lastName) {
                queryString.push("lastName=" + fields.lastName);
            }

            var url = [apiUrl, "contact"].join("/");

            var fullUrl = queryString.length == 0 ? 
                          url : [url, "?", queryString.join("&")].join("");

            return $http.get(fullUrl);
        };

        function getContact(id) {
            return $http.get([apiUrl, "contact", id].join("/"));
        };

        function createContact(model) {
            return $http.post([apiUrl, "contact"].join("/"), model);
        };

        function updateContact(id, model) {
            return $http.put([apiUrl, "contact", id].join("/"), model);
        };

        function deleteContact(id) {
            return $http.delete([apiUrl, "contact", id].join("/"));
        };
    };
})(angular.module("contactManager"));

This is a good practice, because with a service we avoid to inject $http service in all controllers, instead of that, we'll inject RepositoryService because in future we change the url of API, we don't worry about changing all controllers, we'll change only the services.

In controllers directory, we'll create a controller for each operation for contacts:

  • HomeController
  • ContactAddController
  • ContactDetailsController
  • ContactEditController
  • ContactRemoveController

Home

JavaScript
(function (app) {
    "use strict";

    app.controller("HomeController", HomeController);

    HomeController.$inject = ["$location", "toaster", "RepositoryService"];

    function HomeController($location, toaster, repository) {
        var vm = this;

        vm.contacts = [];
        vm.search = {};
        vm.add = add;
        vm.search = search;
        vm.details = details;
        vm.remove = remove;

        toaster.pop("wait", "Loading contacts...");

        repository.getContacts(vm.search).then(function (result) {
            vm.contacts = result.data;
        });

        vm.add = function () {
            $location.path("/contact/add/");
        };

        vm.search = function () {
            repository.getContacts(vm.search).then(function (result) {
                vm.contacts = result.data;
            });
        };
        
        vm.details = function (id) {
            $location.path("/contact/details/" + id);
        };

        vm.remove = function (id) {
            $location.path("/contact/remove/" + id);
        };
    };
})(angular.module("contactManager"));

index.html code file:

HTML
<h2>Contacts</h2>

<p>
    <a ng-click="vm.add()">Add new</a>
</p>

<div class="container">
    <div class="row">
        <div class="form-inline">
            <div class="form-group">
                <input type="text" name="firstName" class="form-control" 
                 placeholder="First name" ng-model="vm.search.firstName" />
            </div>

            <div class="form-group">
                <input type="text" name="middleName" class="form-control" 
                 placeholder="Middle name" ng-model="vm.search.middleName" />
            </div>

            <div class="form-group">
                <input type="text" name="lastName" class="form-control" 
                 placeholder="Last name" ng-model="vm.search.lastName" />
            </div>

            <div class="form-group">
                <select name="pageSize" class="form-control list-box" 
                 ng-model="vm.search.pageSize">
                    <option value="10" selected="selected">10</option>
                    <option value="25">25</option>
                    <option value="50">50</option>
                    <option value="100">100</option>
                </select>
            </div>

            <div class="form-group">
                <button type="button" 
                 class="btn btn-primary glyphicon glyphicon-search" 
                 ng-click="vm.search();" />
            </div>
        </div>
    </div>
</div>

<br />

<table class="table table-hover">
    <tr>
        <th>First name</th>
        <th>Middle name</th>
        <th>Last name</th>
        <th>Phone</th>
        <th>Email</th>
        <th></th>
    </tr>
    <tr ng-repeat="contact in vm.contacts">
        <td>{{ contact.firstName }}</td>
        <td>{{ contact.middleName }}</td>
        <td>{{ contact.lastName }}</td>
        <td>{{ contact.phone }}</td>
        <td>{{ contact.email }}</td>
        <td>
            <div class="dropdown">
                <button class="btn btn-primary btn-lg dropdown-toggle" 
                        type="button" data-toggle="dropdown">
                <span class="caret"></span></button>
                <ul class="dropdown-menu">
                    <li><a ng-click="vm.details(contact._id)">Details</a></li>
                    <li><a ng-click="vm.remove(contact._id)">Remove</a></li>
                </ul>
            </div>
        </td>
    </tr>
</table>

We define the controller and inject all required services: $location and RepositoryService, the first service is for redirect and the second is the API repository.

Please note we aren't using $scope service, instead of scope we are using an alias for controller: vm, in fact in all HTML files, we'll always use vm alias to access the members of controllers.

Add Contact

JavaScript
(function (app) {
    "use strict";

    app.controller("ContactAddController", ContactAddController);

    ContactAddController.$inject = ["$location", "toaster", "RepositoryService"];

    function ContactAddController($location, toaster, repository) {
        var vm = this;

        vm.model = {};

        vm.save = save;
        vm.cancel = cancel;

        function save() {
            toaster.pop("wait", "Saving...");

            repository.createContact(vm.model).then(function (result) {
                toaster.pop("success", "The contact was saved successfully");

                $location.path("/");
            });
        };

        function cancel() {
            $location.path("/");
        };
    };
})(angular.module("contactManager"));

add.html code file:

HTML
<h2>Create</h2>

<form name="form" novalidate ng-submit="vm.save()">
    <div class="form-horizontal">
        <h4>Contact</h4>
        <hr />

        <div class="form-group" ng-class="{ 'has-error': form.firstName.$invalid }">
            <label for="firstName" class="control-label col-md-2">First name</label>
            <div class="col-md-10">
                <input type="text" name="firstName" class="form-control" 
                 required ng-model="vm.model.firstName" />
                <span class="text-danger help-block" 
                 ng-show="form.firstName.$error.required">(*) Required</span>
            </div>
        </div>

        <div class="form-group">
            <label for="middleName" class="control-label col-md-2">Middle name</label>
            <div class="col-md-10">
                <input type="text" name="middleName" class="form-control" 
                 ng-model="vm.model.middleName" />
            </div>
        </div>

        <div class="form-group" ng-class="{ 'has-error': form.lastName.$invalid }">
            <label for="lastName" class="control-label col-md-2">Last name</label>
            <div class="col-md-10">
                <input type="text" name="lastName" class="form-control" 
                 required ng-model="vm.model.lastName" />
                <span class="text-danger help-block" 
                 ng-show="form.lastName.$error.required">(*) Required</span>
            </div>
        </div>

        <div class="form-group" ng-class="{ 'has-error': form.phone.$invalid }">
            <label for="phone" class="control-label col-md-2">Phone</label>
            <div class="col-md-10">
                <input type="text" name="phone" class="form-control" 
                 required ng-model="vm.model.phone" />
                <span class="text-danger help-block" 
                 ng-show="form.phone.$error.required">(*) Required</span>
            </div>
        </div>

        <div class="form-group" ng-class="{ 'has-error': form.email.$invalid }">
            <label for="email" class="control-label col-md-2">Email</label>
            <div class="col-md-10">
                <input type="text" name="email" class="form-control" 
                 required ng-model="vm.model.email" />
                <span class="text-danger help-block" 
                 ng-show="form.email.$error.required">(*) Required</span>
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" ng-disabled="!form.$valid" 
                 class="btn btn-primary" />
                <a ng-click="vm.cancel()">Cancel</a>
            </div>
        </div>
    </div>
</form>

Details for Contact

JavaScript
(function (app) {
    "use strict";

    app.controller("ContactDetailsController", ContactDetailsController);

    ContactDetailsController.$inject = 
    ["$routeParams", "$location", "RepositoryService"];

    function ContactDetailsController($routeParams, $location, repository) {
        var vm = this;

        var id = $routeParams.id;

        vm.model = {};
        vm.edit = edit;
        vm.cancel = cancel;

        repository.getContact(id).then(function (result) {
            vm.model = result.data;
        });

        function edit() {
            $location.path("/contact/edit/" + id);
        };

        function cancel() {
            $location.path("/");
        };
    };
})(angular.module("contactManager"));

details.html code file:

HTML
<h2>Details</h2>

<div>
    <h4>Contact</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>First name</dt>
        <dd>{{ vm.model.firstName }}</dd>

        <dt>Middle name</dt>
        <dd>{{ vm.model.middleName }}</dd>

        <dt>Last name</dt>
        <dd>{{ vm.model.lastName }}</dd>

        <dt>Phone</dt>
        <dd>{{ vm.model.phone }}</dd>

        <dt>Email</dt>
        <dd>{{ vm.model.email }}</dd>
    </dl>
</div>

<p>
    <a ng-click="vm.edit()">Edit</a> | <a ng-click="vm.cancel()">Cancel</a>
</p>

Edit Contact

JavaScript
(function (app) {
    "use strict";

    app.controller("ContactEditController", ContactEditController);

    ContactEditController.$inject = 
           ["$routeParams", "$location", "toaster", "RepositoryService"];

    function ContactEditController($routeParams, $location, toaster, repository) {
        var vm = this;

        var id = $routeParams.id;

        vm.model = {};
        vm.save = save;
        vm.cancel = cancel;

        repository.getContact(id).then(function (result) {
            vm.model = result.data;
        });

        function save() {
            toaster.pop("wait", "Saving...");

            repository.updateContact(id, vm.model).then(function (result) {
                toaster.pop("success", "The changes were saved successfully");

                $location.path("/contact/details/" + id);
            });
        };

        function cancel() {
            $location.path("/contact/details/" + id);
        };
    };
})(angular.module("contactManager"));

edit.html code file:

HTML
<h2>Edit</h2>

<form name="form" novalidate ng-submit="vm.save()">
    <div class="form-horizontal">
        <h4>Contact</h4>
        <hr />

        <div class="form-group" ng-class="{ 'has-error': form.firstName.$invalid }">
            <label for="firstName" class="control-label col-md-2">First name</label>
            <div class="col-md-10">
                <input type="text" name="firstName" 
                 class="form-control" required ng-model="vm.model.firstName" />
                <span class="text-danger help-block" 
                 ng-show="form.firstName.$error.required">(*) Required</span>
            </div>
        </div>

        <div class="form-group">
            <label for="middleName" class="control-label col-md-2">Middle name</label>
            <div class="col-md-10">
                <input type="text" name="middleName" 
                 class="form-control" ng-model="vm.model.middleName" />
            </div>
        </div>

        <div class="form-group" ng-class="{ 'has-error': form.lastName.$invalid }">
            <label for="lastName" class="control-label col-md-2">Last name</label>
            <div class="col-md-10">
                <input type="text" name="lastName" 
                 class="form-control" required ng-model="vm.model.lastName" />
                <span class="text-danger help-block" 
                 ng-show="form.lastName.$error.required">(*) Required</span>
            </div>
        </div>

        <div class="form-group" ng-class="{ 'has-error': form.phone.$invalid }">
            <label for="phone" class="control-label col-md-2">Phone</label>
            <div class="col-md-10">
                <input type="text" name="phone" 
                 class="form-control" required ng-model="vm.model.phone" />
                <span class="text-danger help-block" 
                 ng-show="form.phone.$error.required">(*) Required</span>
            </div>
        </div>

        <div class="form-group" ng-class="{ 'has-error': form.email.$invalid }">
            <label for="email" class="control-label col-md-2">Email</label>
            <div class="col-md-10">
                <input type="text" name="email" class="form-control" 
                 required ng-model="vm.model.email" />
                <span class="text-danger help-block" 
                 ng-show="form.email.$error.required">(*) Required</span>
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" 
                 ng-disabled="!form.$valid" class="btn btn-primary" />
                <a ng-click="vm.cancel()">Cancel</a>
            </div>
        </div>
    </div>
</form>

Remove Contact

JavaScript
(function (app) {
    "use strict";

    app.controller("ContactRemoveController", ContactRemoveController);

    ContactRemoveController.$inject = 
           ["$location", "$routeParams", "toaster", "RepositoryService"];

    function ContactRemoveController($location, $routeParams, toaster, repository) {
        var vm = this;

        var id = $routeParams.id;

        vm.model = {};
        vm.remove = remove;
        vm.cancel = cancel;

        repository.getContact(id).then(function (result) {
            vm.model = result.data;
        });

        function remove() {
            toaster.pop("wait", "Removing...");

            repository.deleteContact(id).then(function (result) {
                toaster.pop("success", "The contact was removed successfully");

                $location.path("/");
            });
        };

        function cancel() {
            $location.path("/");
        };
    };
})(angular.module("contactManager"));

remove.html code file:

HTML
<h2>Remove</h2>

<h3>Are you sure to want remove this record?</h3>

<div>
    <h4>Contact</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>First name</dt>
        <dd>{{ vm.model.firstName }}</dd>

        <dt>Middle name</dt>
        <dd>{{ vm.model.middleName }}</dd>

        <dt>Last name</dt>
        <dd>{{ vm.model.lastName }}</dd>

        <dt>Phone</dt>
        <dd>{{ vm.model.phone }}</dd>

        <dt>Email</dt>
        <dd>{{ vm.model.email }}</dd>
    </dl>

    <div class="form-actions no-color">
        <a ng-click="vm.remove(vm.model._id)" class="btn btn-default">Delete</a>
        <a ng-click="vm.cancel()" class="btn btn-small">Cancel</a>
    </div>
</div>

Finally, we'll have the following structure:

File Structure

Now, in command line, execute this command: node server.js, then open a browser and load the url: http://localhost:3000/, try to add, edit and delete contacts in the app, all ids are generated by MongoDB engine, so we don't need to worry about the ids.

Step 03 - Add Mocker for Contacts

contact.mocker.js code file:

JavaScript
var mongojs = require("mongojs");
var db = mongojs("contactManager", ["contacts"]);

var limit = 1000;

var firstNames = ["James", "John", "Robert", "Michael", "William", 
                  "David", "Richard", "Charles", "Joseph", "Thomas"];

var middleNames = ["Peter", "Lee", "Alexander", "Daniel", "Edward"];

var lastNames = ["Smith", "Johnson", "Williams", "Jones", "Brown", 
                 "Davis", "Miller", "Wilson", "Moore"];

var getRandomValue = function (min, max) {
    return Math.floor(Math.random() * ((max - min) + 1)) + min;
};

var getRandomItem = function (array) {
    return array[Math.floor(Math.random() * array.length)];
};

var phoneMocker = function () {
    var areaCode = getRandomValue(250, 500);
    var phoneNumber = getRandomValue(2000000, 5000000);

    return [areaCode, " ", phoneNumber].join("");
};

var emailMocker = function (contact) {
    var separators = ["", ".", "_", "", ".", "_"];
    var separator = getRandomItem(separators);

    var contactInfo = [contact.firstName, separator, 
                       contact.middleName, separator, contact.lastName].join("");

    var domains = ["outlook.com", "gmail.com", "yahoo.com"];

    return contactInfo.toLowerCase() + "@" + getRandomItem(domains);
};

for (var i = 0; i < limit; i++) {
    var firstName = getRandomItem(firstNames);
    var middleName = getRandomItem(middleNames);
    var lastName = getRandomItem(lastNames);

    var item = {
        firstName: firstName,
        middleName: middleName,
        lastName: lastName,
        phone: phoneMocker()
    };

    item.email = emailMocker(item);

    console.log("Inserting new row...");

    db.contacts.insert(item, function (err, doc) {
        if (err) {
            console.log(err);
        }
    });
}

To generate data in collection, we can run contact.mocker.js file from command line: node contact.mocker.js, also we can set the quantity of records, in this sample, the limit is 1000; the first names, middle names and last names are from statistics

How it works? Iterates a for to create all contacts, for each contact, gets the first name, middle name and last name in random way from arrays, in phone takes the number in a range and the email is generated according to names split by a random separator.

Code Improvements

  • Add max length and regular expression validations for fields
  • Add unit testing for AngularJS controllers
  • Add authentication

Points of Interest

  • JavaScript is untyped language but we want to develop in strong typed language we can use TypeScript, TypeScript is a superset of JavaScript
  • For JavaScript, we are using strict mode, according to best practices, this makes it easier to write "secure" JavaScript
  • If we need to access another type of database, we can install the required package and change the logic to access database.

Related Links

History

  • 5th February, 2017: Initial version
  • 6th February, 2017: Added instructions to setup environment
  • 22nd February, 2017: Added contact mocker
  • 3rd October, 2017: Changes in definitions for functions

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)