Book Image

MEAN Blueprints

By : Robert Onodi
Book Image

MEAN Blueprints

By: Robert Onodi

Overview of this book

The MEAN stack is a combination of the most popular web development frameworks available—MongoDB, Angular, Express, and Node.js used together to offer a powerful and comprehensive full stack web development solution. It is the modern day web dev alternative to the old LAMP stack. It works by allowing AngularJS to handle the front end, and selecting Mongo, Express, and Node to handle the back-end development, which makes increasing sense to forward-thinking web developers. The MEAN stack is great if you want to prototype complex web applications. This book will enable you to build a better foundation for your AngularJS apps. Each chapter covers a complete, single, advanced end-to-end project. You’ll learn how to build complex real-life applications with the MEAN stack and few more advanced projects. You will become familiar with WebSockets and build real-time web applications, as well as create auto-destructing entities. Later, we will combine server-side rendering techniques with a single page application approach. You’ll build a fun project and see how to work with monetary data in Mongo. You will also find out how to a build real-time e-commerce application. By the end of this book, you will be a lot more confident in developing real-time, complex web applications using the MEAN stack.
Table of Contents (13 chapters)
MEAN Blueprints
Credits
About the Author
About the Reviewer
www.PacktPub.com
Preface
Index

Managing contacts


Now that we have the files necessary to start development and add features, we can start implementing all of the business logic related to managing contacts. To do this, we first need to define the data model of a contact.

Creating the contact mongoose schema

Our system needs some sort of functionality to store the possible clients or just contact persons of other companies. For this, we are going to create a contact schema that will represent the same collection storing all the contacts in MongoDB. We are going to keep our contact schema simple. Let's create a model file in contact-manager/app/models/contact.js, which will hold the schema, and add the following code to it:

'use strict';

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

var ContactSchema = new Schema({
  email:  {
    type: String
  },
  name: {
    type: String
  },
  city: {
    type: String
  },
  phoneNumber: {
    type: String
  },
  company: {
    type: String
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// compile and export the Contact model
module.exports = mongoose.model('Contact', ContactSchema);

The following table gives a description of the fields in the schema:

Field

Description

email

The e-mail of the contact

name

The full name of the contact

company

The name of the company at which the contact person works

phoneNumber

The full phone number of the person or company

city

The location of the contact

createdAt

The date at which the contact object was created

All our model files will be registered in the following configuration file, found under contact-manager/config/models.js. The final version of this file will look something like this:

'use strict';

module.exports.init = initModels;

function initModels(app) {
  let modelsPath = app.get('root') + '/app/models/';

  ['user', 'contact'].forEach(function(model) {
    require(modelsPath + model);
  });
};

Describing the contact route

In order to communicate with the server, we need to expose routes for client applications to consume. These are going to be endpoints (URIs) that respond to client requests. Mainly, our routes will send a JSON response.

We are going to start by describing the CRUD functionality of the contact module. The routes should expose the following functionalities:

  • Create a new contact

  • Get a contact by ID

  • Get all contacts

  • Update a contact

  • Delete a contact by ID

We are not going to cover bulk insert and delete in this application.

The following table shows how these operations can be mapped to HTTP routes and verbs:

Route

Verb

Description

Data

/contacts

POST

Create a new contact

email, name, company, phoneNumber, and city

/contacts

GET

Get all contacts from the system

 

/contacts/<id>

GET

Get a particular contact

 

/contacts/<id>

PUT

Update a particular contact

email, name, company, phoneNumber, and city

/contacts/<id>

DELETE

Delete a particular contact

 

Following the earlier table as a guide, we are going to describe our main functionality and test using Mocha. Mocha allows us to describe the features that we are implementing by giving us the ability to use a describe function that encapsulates our expectations. The first argument of the function is a simple string that describes the feature. The second argument is a function body that represents the description.

You have already created a folder called contact-manger/tests. In your tests folder, create another folder called integration. Create a file called contact-manager/tests/integration/contact_test.js and add the following code:

'use strict';

/**
 * Important! Set the environment to test
 */
process.env.NODE_ENV = 'test';

const http = require('http');
const request = require('request');
const chai = require('chai');
const userFixture = require('../fixtures/user');
const should = chai.should();

let app;
let appServer;
let mongoose;
let User;
let Contact;
let config;
let baseUrl;
let apiUrl;

describe('Contacts endpoints test', function() {

  before((done) => {
    // boot app
    // start listening to requests
  });

  after(function(done) {
    // close app
    // cleanup database
    // close connection to mongo
  });

  afterEach((done) => {
    // remove contacts
  });

  describe('Save contact', () => {});

  describe('Get contacts', () => {});

  describe('Get contact', function() {});

  describe('Update contact', function() {});

  describe('Delete contact', function() {});
});

In our test file, we required our dependencies and used Chai as our assertion library. As you can see, besides the describe() function, mocha gives us additional methods: before(), after(), beforeEach(), and afterEach().

These are hooks and they can be async or sync, but we are going to use the async version of them. Hooks are useful for preparing preconditions before running tests; for example, you can populate your database with mock data or clean it up.

In the main description body, we used three hooks: before(), after(), and afterEach(). In the before() hook, which will run before any of the describe() functions, we set up our server to listen on a given port, and we called the done() function when the server started listening.

The after() function will run after all the describe() functions have finished running and will stop the server from running. Now, the afterEach() hook will run after each describe() function, and it will grant us the ability to remove all the contacts from the database after running each test.

The final version can be found in the code bundle of the application. You can still follow how we add all the necessary descriptions.

Creating a contact

We also added four to five individual descriptions that will define CRUD operations from the earlier table. First, we want to be able to create a new contact. Add the following code to the test case:

  describe('Create contact', () => {
    it('should create a new contact', (done) => {
      request({
        method: 'POST',
        url: `${apiUrl}/contacts`,
        form: {
          'email': '[email protected]',
          'name': 'Jane Doe'
        },
        json:true
      }, (err, res, body) => {
        if (err) throw err;

        res.statusCode.should.equal(201);
        body.email.should.equal('[email protected]');
        body.name.should.equal('Jane Doe');
        done();
      });
    });
  });

Getting contacts

Next, we want to get all contacts from the system. The following code should describe this functionality:

  describe('Get contacts', () => {
    before((done) => {
      Contact.collection.insert([
        { email: '[email protected]' },
        { email: '[email protected]' }
      ], (err, contacts) => {
        if (err) throw err;

        done();
      });
    });

    it('should get a list of contacts', (done) => {
      request({
        method: 'GET',
        url: `${apiUrl}/contacts`,
        json:true
      }, (err, res, body) => {
        if (err) throw err;

        res.statusCode.should.equal(200);
        body.should.be.instanceof(Array);
        body.length.should.equal(2);
        body.should.contain.a.thing.with.property('email', '[email protected]');
        body.should.contain.a.thing.with.property('email', '[email protected]');
        done();
      });
    });
  });

As you can see, we've also added a before() hook in the description. This is absolutely normal and can be done. Mocha permits this behavior in order to easily set up preconditions. We used a bulk insert, Contact.collection.insert(), to add data into MongoDB before getting all the contacts.

Getting a contact by ID

When getting a contact by ID, we would also want to check whether the inserted ID meets our ObjectId criteria. If a contact is not found, we will want to return a 404 HTTP status code:

  describe('Get contact', function() {
    let _contact;

    before((done) => {
      Contact.create({
        email: '[email protected]'
      }, (err, contact) => {
        if (err) throw err;

        _contact = contact;
        done();
      });
    });

    it('should get a single contact by id', (done) => {
      request({
        method: 'GET',
        url: `${apiUrl}/contacts/${_contact.id}`,
        json:true
      }, (err, res, body) => {
        if (err) throw err;

        res.statusCode.should.equal(200);
        body.email.should.equal(_contact.email);
        done();
      });
    });

    it('should not get a contact if the id is not 24 characters', (done) => {
      request({
        method: 'GET',
        url: `${apiUrl}/contacts/U5ZArj3hjzj3zusT8JnZbWFu`,
        json:true
      }, (err, res, body) => {
        if (err) throw err;

        res.statusCode.should.equal(404);
        done();
      });
    });
  });

We used the .create() method. It's more convenient to use it for single inserts, to prepopulate the database with data. When getting a single contact by ID we want to ensure that it's a valid ID, so we added a test which should reflect this and get a 404 Not Found response if it's invalid, or no contact was found.

Updating a contact

We also want to be able to update an existing contact with a given ID. Add the following code to describe this functionality:

  describe('Update contact', () => {
    let _contact;

    before((done) => {
      Contact.create({
        email: '[email protected]'
      }, (err, contact) => {
        if (err) throw err;

        _contact = contact;
        done();
      });
    });

    it('should update an existing contact', (done) => {
      request({
        method: 'PUT',
        url: `${apiUrl}/contacts/${_contact.id}`,
        form: {
          'name': 'Jane Doe'
        },
        json:true
      }, (err, res, body) => {
        if (err) throw err;

        res.statusCode.should.equal(200);
        body.email.should.equal(_contact.email);
        body.name.should.equal('Jane Doe');
        done();
      });
    });
  });

Removing a contact

Finally, we'll describe the remove contact operation (DELETE from CRUD) by adding the following code:

  describe('Delete contact', () => {
    var _contact;

    before((done) => {
      Contact.create({
        email: '[email protected]'
      }, (err, contact) => {
        if (err) throw err;

        _contact = contact;
        done();
      });
    });

    it('should update an existing contact', (done) => {
      request({
        method: 'DELETE',
        url: `${apiUrl}/contacts/${_contact.id}`,
        json:true
      }, (err, res, body) => {
        if (err) throw err;

        res.statusCode.should.equal(204);
        should.not.exist(body);
        done();
      });
    });
  });

After deleting a contact, the server should respond with an HTTP 204 No Content status code, meaning that the server has successfully interpreted the request and processed it, but no content should be returned due to the fact that the contact was deleted successfully.

Running our tests

Suppose we run the following command:

$ mocha test/integration/contact_test.js

At this point, we will get a bunch of HTTP 404 Not Found status codes, because our routes are not implemented yet. The output should be similar to something like this:

  Contact
    Save contact
      1) should save a new contact
    Get contacts
      2) should get a list of contacts
    Get contact
      3) should get a single contact by id
      √ should not get a contact if the id is not 24 characters
    Update contact
      4) should update an existing contact
    Delete contact
      5) should update an existing contact

  1 passing (485ms)
  5 failing
  1) Contact Save contact should save a new contact:

      Uncaught AssertionError: expected 404 to equal 201
      + expected - actual

      +201
      -404

Implementing the contact routes

Now, we'll start implementing the contact CRUD operations. We'll begin by creating our controller. Create a new file, contact-manager/app/controllers/contact.js, and add the following code:

'use strict';

const _ = require('lodash');
const mongoose = require('mongoose');
const Contact = mongoose.model('Contact');
const ObjectId = mongoose.Types.ObjectId;

module.exports.create = createContact;
module.exports.findById = findContactById;
module.exports.getOne = getOneContact;
module.exports.getAll = getAllContacts;
module.exports.update = updateContact;
module.exports.remove = removeContact;

function createContact(req, res, next) {
  Contact.create(req.body, (err, contact) => {
    if (err) {
      return next(err);
    }

    res.status(201).json(contact);
  });
}

What the preceding code does is export all methods of the controller for CRUD operations. To create a new contact, we use the create() method from the Contact schema.

We are returning a JSON response with the newly created contact. In case of an error, we just call the next() function with the error object. We will add a special handler to catch all of our errors later.

Let's create a new file for our routes, contact-manager/app/routes/contacts.js. The following lines of code should be a good start for our router:

'use strict';

const express = require('express');
const router = express.Router();
const contactController = require('../controllers/contact');

router.post('/contacts', auth.ensured, contactController.create);

module.exports = router;

Suppose we run our test now using this, like:

$ mocha tests/integration/contact_test.js

We should get something similar to the following:

Contact
  Create contact
      √ should save a new contact
  Get contacts
      1) should get a list of contacts
  Get contact
      2) should get a single contact by id
      √ should not get a contact if the id is not 24 characters
    Update contact
      3) should update an existing contact
  Delete contact
      4) should update an existing contact


  2 passing (502ms)
  4 failing

Adding all endpoints

Next, we will add the rest of the routes, by adding the following code into the contact-manager/app/routes/contact.js file:

router.param('contactId', contactController.findById);

router.get('/contacts', auth.ensured, contactController.getAll);
router.get('/contacts/:contactId', auth.ensured, contactController.getOne);
router.put('/contacts/:contactId', auth.ensured, contactController.update);
router.delete('/contacts/:contactId', auth.ensured, contactController.remove);

We defined all the routes and also added a callback trigger to the contactId route parameter. In Express, we can add callback triggers on route parameters using the param() method with the name of a parameter and a callback function.

The callback function is similar to any normal route callback, but it gets an extra parameter representing the value of the route parameter. A concrete example would be as follows:

app.param('contactId', function(req, res, next, id) { 
  // do something with the id ...
});

Following the preceding example, when :contactId is present in a route path, we can map a contact loading logic and provide the contact to the next handler.

Finding a contact by ID

We are going to add the rest of the missing functionalities in our controller file, located at contact-manager/app/controllers/contact.js:

function findContactById(req, res, next, id) {
  if (!ObjectId.isValid(id)) {
    res.status(404).send({ message: 'Not found.'});
  }

  Contact.findById(id, (err, contact) => {
    if (err) {
      next(err);
    } else if (contact) {
      req.contact = contact;
      next();
    } else {
      next(new Error('failed to find contact'));
    }
  });
}

The preceding function is a special case. It will get four parameter, and the last one will be the ID matching the triggered parameters value.

Getting contact information

To get all contacts, we are going to query the database. We will sort our results based on the creation date. One good practice is to always limit your returned dataset's size. For that, we use a MAX_LIMIT constant:

function getAllContacts(req, res, next) {
  const limit = +req.query.limit || MAX_LIMIT;
  const skip = +req.query.offset || 0;
  const query = {};

  if (limit > MAX_LIMIT) {
    limit = MAX_LIMIT;
  }

  Contact
  .find(query)
  .skip(skip)
  .limit(limit)
  .sort({createdAt: 'desc'})
  .exec((err, contacts) => {
    if (err) {
      return next(err);
    }

    res.json(contacts);
  });
}

To return a single contact, you can use the following code:

function getOneContact(req, res, next) {
  if (!req.contact) {
    return next(err);
  }

  res.json(req.contact);
}

Theoretically, we'll have the :contactId parameter in a route definition. In that case, the param callback is triggered, populating the req object with the requested contact.

Updating a contact

The same principle is applied when updating a contact; the requested entity should be populated by the param callback. We just need to assign the incoming data to the contact object and save the changes into MongoDB:

function updateContact(req, res, next) {
  let contact = req.contact;
  _.assign(contact, req.body);

  contact.save((err, updatedContact) => {
    if (err) {
      return next(err);
    }

    res.json(updatedContact);
  });
}

Removing a contact

Removing a contact should be fairly simple, as it has no dependent documents. So, we can just remove the document from the database, using the following code:

function removeContact(req, res, next) {
  req.contact.remove((err) => {
    if (err) {
      return next(err);
    }

    res.status(204).json();
  });
}

Running the contact test

At this point, we should have implemented all the requirements for managing contacts on the backend. To test everything, we run the following command:

$ mocha tests/integration/contact.test.js

The output should be similar to this:

  Contact

    Save contact
      √ should save a new contact
    Get contacts
      √ should get a list of contacts
    Get contact
      √ should get a single contact by id
      √ should not get a contact if the id is not 24 characters
    Update contact
      √ should update an existing contact
    Delete contact
      √ should update an existing contact


  6 passing (576ms)

This means that all the tests have passed successfully and we have implemented all the requirements.