- MEAN Blueprints
- Robert Onodi
- 2377字
- 2021-07-16 10:40:19
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:

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:

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.
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': 'jane.doe@test.com', 'name': 'Jane Doe' }, json:true }, (err, res, body) => { if (err) throw err; res.statusCode.should.equal(201); body.email.should.equal('jane.doe@test.com'); body.name.should.equal('Jane Doe'); done(); }); }); });
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: 'jane.doe@test.com' }, { email: 'john.doe@test.com' } ], (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', 'jane.doe@test.com'); body.should.contain.a.thing.with.property('email', 'john.doe@test.com'); 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.
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: 'john.doe@test.com' }, (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.
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: 'jane.doe@test.com' }, (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(); }); }); });
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: 'jane.doe@test.com' }, (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.
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
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.
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.
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.
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 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.
- Web程序設(shè)計(jì)及應(yīng)用
- 三維圖形化C++趣味編程
- Mastering Rust
- Python機(jī)器學(xué)習(xí)編程與實(shí)戰(zhàn)
- SQL Server 2012數(shù)據(jù)庫管理與開發(fā)項(xiàng)目教程
- 深入理解Elasticsearch(原書第3版)
- .NET 3.5編程
- Webpack實(shí)戰(zhàn):入門、進(jìn)階與調(diào)優(yōu)
- NoSQL數(shù)據(jù)庫原理
- Scratch趣味編程:陪孩子像搭積木一樣學(xué)編程
- Bootstrap for Rails
- TypeScript 2.x By Example
- 軟件測試技術(shù)
- Java程序設(shè)計(jì)實(shí)用教程(第2版)
- Wearable:Tech Projects with the Raspberry Pi Zero