MEAN Stack Tutorial - Part 2
After the basic setup, we start coding on the server side of the "stack"
CODING THE SERVER SIDE
We need the add the logic to the server. We will be using Node.js and MongoDB for the server side and to start, we will go to the Category model because is the easiest.
Simple table with CategoryID, Name and Description
We are only interested in the Category Name and Category description from this template. What we want to do is a CRUD (create, read, update and delete) API so, what we are going to see in this tutorial is how to interact with the DB, representing the data through a model, create a container for the logic and exposing the logic to the endpoint.
Now, let’s see what the CategoryAPI is intended to do:
Category API
unauthenticated create request with
valid category
- returns success status
- returns category details including new id
- is saved in database
empty name
- returns invalid status
- returns validation message
name longer than 15 chars in length
- returns invalid status
- returns validation message
duplicate name
- returns invalid status
- returns validation message
unauthenticated get request with
no parameters
- lists all categories in alphabetical order
valid category id
- returns success status
- returns the expected category
invalid category id
- returns not found status
unauthenticated update request with
valid category
- returns success status
- returns category details
- is updated in database
- only updates specified record
empty category name
- returns invalid status
- returns validation message
category name longer than 15 chars in length
- returns invalid status
- returns validation message
duplicate category name
- returns invalid status
- returns validation message
unauthenticated delete request with
valid category id
- returns success status
- returns category details
- is deleted from database
invalid category id
- returns not found status
Express Framework
We will use Express for the server framework in order to start and configure the server, configure the routes and route handlers for our logic and return static content. Express has some main files which are:
server.js- Grunt task calls this entry point.
config/express.js- This is where all the config is.
GETTING STARTED
Since there are a few things to do, we will do them one by one, first we will:
Add a Model
Models are more or less the same as classes in many programming languages and this one will represent the data of the Categories Database. In the category model shell run the following:
yo meanjs:express-model category
Some new files will be created. The app/models/category.server.model.js for the model and also the app/tests/category.server.model.test.js for the model test. So, the Category model will look like this now:
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
* Category Schema
*/
var CategorySchema = new Schema({
// Category model fields
// ... (we will add properties here soon...)
});
// add the model name and schema to the mongoose model.
mongoose.model('Category', CategorySchema);
By using the function require we are including the functionality of another module. This is the way to do it in Node.js.
As explained in the code, the line:
mongoose.model('Category', CategorySchema);
Will add the model name and schema to the mongoose instance and the model Category will be also available to any other modules when they use the word “require” mongoos
Testing the Model
Now we should run the test that we got previously. Use the command npm test to do it and the terminal will should perform it. This test should go through without problems but since there are no properties in the model, that’s not really useful.
Now go to app/tests/category.server.model.test.js and replace everything with the following snippet:
'use strict';
/**
* Module dependencies.
*/
var should = require('should'),
mongoose = require('mongoose'),
Category = mongoose.model('Category');
/**
* Unit tests
*/
describe('Category Model', function() {
describe('Saving', function() {
it('saves new record');
it('throws validation error when name is empty');
it('throws validation error when name longer than 15 chars');
it('throws validation error for duplicate category name');
});
});
Now by running the test again we will get the following messages:
Category Model
Saving
- saves new record
- throws validation error when name is empty
- throws validation error when name longer than 15 chars
- throws validation error for duplicate category name
Next thing is to add properties to the model.
Working Out the Logic
We should add the properties to the model before implementing the tests. We will use the mongoose library to tinker with MongoDB.
Now copy the following snippet to app/models/category.server.model.js. In the same code you can also view the explanation of the most important lines.
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
validation = require('./validation.server.model');
/**
* Category Schema
*/
var CategorySchema = new Schema({
created: {
type: Date,
default: Date.now
},
description: {
type: String,
default: '',
trim: true
},
name: {
type: String,
default: '',
trim: true,
unique : true,
required: 'name cannot be blank',
validate: [validation.len(15), 'name must be 15 chars in length or less']
}
});
mongoose.model('Category', CategorySchema);
And now paste the following to the app/tests/category.server.model.test.js and now the test will pass
/**
* Module dependencies.
*/
var should = require('should'),
mongoose = require('mongoose'),
Category = mongoose.model('Category');
/**
* Unit tests
*/
describe('Category Model', function() {
describe('Saving', function() {
it('saves new record', function(done) {
var category = new Category({
name: 'Beverages',
description: 'Soft drinks, coffees, teas, beers, and ales'
});
category.save(function(err, saved) {
should.not.exist(err);
done();
});
});
it('throws validation error when name is empty', function(done) {
var category = new Category({
description: 'Soft drinks, coffees, teas, beers, and ales'
});
category.save(function(err) {
should.exist(err);
err.errors.name.message.should.equal('name cannot be blank');
done();
});
});
it('throws validation error when name longer than 15 chars', function(done) {
var category = new Category({
name: 'Grains/Cereals/Chocolates'
});
category.save(function(err, saved) {
should.exist(err);
err.errors.name.message.should.equal('name must be 15 chars in length or less');
done();
});
});
it('throws validation error for duplicate category name', function(done) {
var category = new Category({
name: 'Beverages'
});
category.save(function(err) {
should.not.exist(err);
var duplicate = new Category({
name: 'Beverages'
});
duplicate.save(function(err) {
err.err.indexOf('$name').should.not.equal(-1);
err.err.indexOf('duplicate key error').should.not.equal(-1);
should.exist(err);
done();
});
});
});
});
afterEach(function(done) {
// NB this deletes ALL categories (but is run against a test database)
Category.remove().exec();
done();
});
});
We have called a module “should” and that’s an assertion library. Should.js is a common choice to use with Mocha because this one doesn’t include one.
As long as the database is from Mongoose API we can save is calling .save().
Adding a Controller
We will create our controller to hold the logic and interact with our Category model using Express. First, we will generate the template for the controller with the yo generator.
yo meanjs:express-controller categories
Now, we can check out app/controllers/categories.server.controller.js and it will look like this:
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
_ = require('lodash');
/**
* Create a Category
*/
exports.create = function(req, res) {
};
/**
* Show the current Category
*/
exports.read = function(req, res) {
};
/**
* Update a Category
*/
exports.update = function(req, res) {
};
/**
* Delete an Category
*/
exports.delete = function(req, res) {
};
/**
* List of Categories
*/
exports.list = function(req, res) {
};
This is a basic structure for our purpose but we still have to fill the blanks. But first we must create a route so that our controller can reach HTTP:
Adding a Route
First we must create it with yo. This is a express one.
yo meanjs:express-route categories
And now we can check out the file in app/routes/categories.server.routes.js
'use strict';
module.exports = function(app) {
// Routing logic
// ...
};
Now we need it to output JSON to url http://localhost:3000/categories. Change the contents of the file to:
'use strict';
module.exports = function(app) {
app.route('/categories')
.get(function (request, response) {
response.json([{ name: 'Beverages' }, { name: 'Condiments' }]);
});
};
We can see now the arguments request and response. These come in the express and the object has the data from HTTP requests and the response object makes the controller change the state and then it is returned to the client.
If now we make a get request to http://localhost:3000/categories the output will be:
[
{"name":"Beverages"},
{"name":"Condiments"}
]
Ok, this is how all of this is working. We attached the route detail to the express framework instance and then when a request from HTTP GET is received for the url /categories this is the function used to handle the whole process. Since we plan to send the data in JSON format to the client we have called the function .json.
So, now we have a few parts of our API and we need to know how all of these parts work together. To put it simple, the route is in charge of the interactions with the HTTP (the web) but it doesn’t do anything with the model. The interactions with the model are made through the controller. There is a name for this structure, separation of concerns so, here is how each piece is working. The model is basically what it’s in the DB, the controller is the one in charge of any changes to the model and the route just handles the interactions with the web and passes them to the controller.
List Categories
We now need to take a look at the code in charge of the list of the categories for the list function in the controller. The next code is to be pasted into app/controllers/categories.server.controller.js:
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
errorHandler = require('./errors.server.controller'),
Category = mongoose.model('Category'),
_ = require('lodash');
/**
* Create a Category
*/
exports.create = function(req, res) {
};
/**
* Show the current Category
*/
exports.read = function(req, res) {
};
/**
* Update a Category
*/
exports.update = function(req, res) {
};
/**
* Delete an Category
*/
exports.delete = function(req, res) {
};
/**
* List of Categories
*/
exports.list = function(req, res) {
Category.find().exec(function(err, categories) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(categories);
}
});
};
And now we modify it so it interacts with the controller in app/routes/categories.server.routes.js:
'use strict';
module.exports = function(app) {
var categories = require('../../app/controllers/categories.server.controller');
app.route('/categories')
.get(categories.list);
};
We need to use the word require as a reference to the file path in order to include additional functionalities we need in our controller.
If we check the URL again at http://localhost:3000/categories we will just get an empty array because we still have nothing inside.
Creating Categories
Our list before was empty because we still haven’t created any. So, in order to create categories we have first to add the functionality required to do so. Now go to the controller and update the create method with the following code:
/**
* Create a Category
*/
exports.create = function(req, res) {
var category = new Category(req.body);
category.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.status(201).json(category);
}
});
};
Now we need to update the route so that it calls the create function we just made
'use strict';
module.exports = function(app) {
var categories = require('../../app/controllers/categories.server.controller');
app.route('/categories')
.get(categories.list)
.post(categories.create);
};
And now it’s time for issuing a POST request to the endpoint with the JSON at http://localhost:3000/categories
{
"name" : "Beverages",
"description" : "Soft drinks, coffees, teas, beers, and ales"
}
This is a cURL POST that will come handy:
curl -H "Content-Type: application/json" -d '{"name" : "Beverages","description" : "Soft drinks, coffees, teas, beers, and ales"}' http://localhost:3000/categories
It may happen to some Windows users that they will get problems when using cURL with JSON so, it is better to use the Postman client and set the Content-Type header to application/json. This should solve the problem and you should get a response looking like the following:
{
"__v":0,
"_id":"54d2b9ca3c3113ca6fb9ba3b",
"name":"Beverages",
"description":"Soft drinks, coffees, teas, beers, and ales",
"created":"2015-02-05T00:31:06.127Z"
}
If we now browse to our endpoint at http://localhost:3000/categories we will see the category we just created. To do so and if you use Google Chrome it is much better to use the Postman REST Client when we interact with the APIs.
To prevent update collisions Mongoose provides with the property __v. MongoDB generates the property _id. These are beyond the scope of this tutorial because we will need to explain what is sharding.
Getting the Category by ID
The next functionality is to get the category by ID. Go to the controller and paste the following snippet:
/**
* Show the current Category
*/
exports.read = function(req, res) {
Category.findById(req.params.categoryId).exec(function(err, category) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
if (!category) {
return res.status(404).send({
message: 'Category not found'
});
}
res.json(category);
}
});
};
This is how Express works: In this case we are using the params object attached to the request object to access the category ID. Now, we will set the categoryId configuration with the route:
'use strict';
module.exports = function(app) {
var categories = require('../../app/controllers/categories.server.controller');
app.route('/categories')
.get(categories.list)
.post(categories.create);
// the categoryId param is added to the params object for the request
app.route('/categories/:categoryId')
.get(categories.read);
};
It is time to make a GET request at http://localhost:3000/categories/54d2b9ca3c3113ca6fb9ba3b and we shall get the category by ID as it was planned.
More stuff to do
We have now the basic functionalities but still we have to implement Update and Delete functions. In order to do so, change the values of the following files accordingly.
Categories.server.controller.js
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
errorHandler = require('./errors.server.controller'),
Category = mongoose.model('Category'),
_ = require('lodash');
/**
* Create a Category
*/
exports.create = function(req, res) {
var category = new Category(req.body);
category.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.status(201).json(category);
}
});
};
/**
* Show the current Category
*/
exports.read = function(req, res) {
res.json(req.category);
};
/**
* Update a Category
*/
exports.update = function(req, res) {
var category = req.category;
category = _.extend(category, req.body);
category.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(category);
}
});
};
/**
* Delete an Category
*/
exports.delete = function(req, res) {
var category = req.category;
category.remove(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(category);
}
});
};
/**
* List of Categories
*/
exports.list = function(req, res) {
Category.find().sort('name').exec(function(err, categories) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(categories);
}
});
};
/**
* Category middleware
*/
exports.categoryByID = function(req, res, next, id) {
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).send({
message: 'Category is invalid'
});
}
Category.findById(id).exec(function(err, category) {
if (err) return next(err);
if (!category) {
return res.status(404).send({
message: 'Category not found'
});
}
req.category = category;
next();
});
};
Then categories.server.routes.js:
'use strict';
module.exports = function(app) {
var categories = require('../../app/controllers/categories.server.controller');
app.route('/categories')
.get(categories.list)
.post(categories.create);
app.route('/categories/:categoryId')
.get(categories.read)
.put(categories.update)
.delete(categories.delete);
// Finish by binding the article middleware
// What's this? Where the categoryId is present in the URL
// the logic to 'get by id' is handled by this single function
// and added to the request object i.e. request.category.
app.param('categoryId', categories.categoryByID);
};
And finally, categories.server.model.js
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
* Validation
*/
function validateLength (v) {
// a custom validation function for checking string length
return v.length <= 15;
}
/**
* Category Schema
*/
var CategorySchema = new Schema({
created: { // the property name
type: Date, // types are defined e.g. String, Date, Number - http://mongoosejs.com/docs/guide.html
default: Date.now
},
description: {
type: String,
default: '',
trim: true // types have specific functions e.g. trim, lowercase, uppercase - http://mongoosejs.com/docs/api.html#schema-string-js
},
name: {
type: String,
default: '',
trim: true,
unique : true,
required: 'name cannot be blank',
validate: [validateLength, 'name must be 15 chars in length or less'] // wires into our custom validator function - http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate
}
});
// Expose the model to other objects (similar to a 'public' setter).
mongoose.model('Category', CategorySchema);
Here you are some cURL test scripts for your use. Remember to change the ID number by the one on your local machine.
For Update:
curl -H "Content-Type: application/json" -X PUT -d '{"name" : "Beverages","description" : "Soft drinks, coffees, teas, beers, wines and ales"}' http://localhost:3000/categories/54d82f5489b29df55a3c6124
To delete:
curl -H "Content-Type: application/json" -X DELETE http://localhost:3000/categories/54d82f5489b29df55a3c6124
Authentication
The meanjs template comes with some user’s functionality included with the logics for authorization and authentication. Take a look at them in /app/controllers/users. From this functionality we can use the logic to secure routes when users are not authenticated. To do this, we just have to change the category routes like in the following snippet:
'use strict';
module.exports = function(app) {
var categories = require('../../app/controllers/categories.server.controller');
var users = require('../../app/controllers/users.server.controller');
app.route('/categories')
.get(categories.list)
.post(users.requiresLogin, categories.create);
app.route('/categories/:categoryId')
.get(categories.read)
.put(users.requiresLogin, categories.update)
.delete(users.requiresLogin, categories.delete);
// Finish by binding the article middleware
app.param('categoryId', categories.categoryByID);
};
Basically we changed it to add users.requiresLogin.
In the users.server.controller you can find the code for the method used:
/**
* Require login routing middleware
*/
exports.requiresLogin = function(req, res, next) {
if (!req.isAuthenticated()) {
return res.status(401).send({
message: 'User is not logged in'
});
}
next();
};
We will call the function next() when the user is authenticated because this function is the method we use as category controller. So, if the user is not authenticated next() is not executed because the method of the controller returns and this secures it.
Middleware is an important part for the functionality to be put altogether in Express. Several layers of security, logging or other middleware can be in front of the controller for more security. Now, if you were wondering how the req.isAuthenticated is being set to true or false, the 3rd party library called Passport is the middleware in charge of the authentication for Node.js.
And this ends the server side logic of our API and now we move to the next part and tinker with the MongoDB basics.
Recent Stories
Top DiscoverSDK Experts
Compare Products
Select up to three two products to compare by clicking on the compare icon () of each product.
{{compareToolModel.Error}}
{{CommentsModel.TotalCount}} Comments
Your Comment