app.use(bodyParser.json()); app.use(cookieParser('3CCC4ACD-6ED1-4844-9217-82131BDCB239')); app.use(session({secret: '2C44774A-D649-4D44-9535-46E296EF984F'})); app.use(everyauth.middleware()); app.use(bodyParser.urlencoded()); app.use(methodOverride()); app.use(require('stylus').middleware(__dirname + '/public')); app.use(express.static(path.join(__dirname, 'public'))); app.use(function(req, res, next) {
if (req.session && req.session.admin) { res.locals.admin = true;
} next(); });
var authorize = function(req, res, next) { if (req.session && req.session.admin) return next(); else return res.send(401); }; if ('development' === app.get('env')) { app.use(errorHandler()); } app.get('/', routes.index); app.get('/login', routes.user.login); app.post('/login', routes.user.authenticate); app.get('/logout', routes.user.logout);
app.get('/admin', authorize, routes.article.admin); app.get('/post', authorize, routes.article.post);
app.post('/post', authorize, routes.article.postArticle); app.get('/articles/:slug', routes.article.show); app.all('/api', authorize); app.get('/api/articles', routes.article.list); app.post('/api/articles', routes.article.add); app.put('/api/articles/:id', routes.article.edit); app.del('/api/articles/:id', routes.article.del); app.all('*', function(req, res) {
res.send(404); })
166
var server = http.createServer(app); var boot = function () {
server.listen(app.get('port'), function(){
console.info('Express server listening on port ' + app.get('port')); });
}
var shutdown = function() { server.close(); } if (require.main === module) { boot(); } else {
console.info('Running app as a module') exports.boot = boot;
exports.shutdown = shutdown; exports.port = app.get('port'); }
There are three files in the models folder: 1. index.js: exposes models to app.js
2. article.js: includes article schemas, methods, and models 3. user.js: includes the user schema and its model
The index.js file is as follows:
exports.Article = require('./article'); exports.User = require('./user');
The article.js file starts with the inclusion:
var mongoose = require('mongoose');
Then, the schema itself is:
var articleSchema = new mongoose.Schema({ title: { type: String, required: true, validate: [ function(value) { return value.length<=120 },
'Title is too long (120 max)' ],
default: 'New Post' }, text: String, published: { type: Boolean, default: false },
167
slug: { type: String, set: function(value) { return value.toLowerCase().replace(' ', '-') } } });In the schema above, title is required and it’s limited to 120 characters with validate. The published defaults to false if not specified upon object creation. The slug should never have spaces due to the set method.
To illustrate code reuse, we abstract the find method from the routes (routes/article.js) into the model (models/article.js). This can be done with all database methods:
articleSchema.static({ list: function(callback){
this.find({}, null, {sort: {_id:-1}}, callback); }
})
The, we compile the schema and methods into a model:
module.exports = mongoose.model('Article', articleSchema);
The full source code of article.js is as follows:
var mongoose = require('mongoose'); var articleSchema = new mongoose.Schema({ title: { type: String, required: true, validate: [ function(value) { return value.length<=120 },
'Title is too long (120 max)' ],
default: 'New Post' }, text: String, published: { type: Boolean, default: false }, slug: { type: String, set: function(value){ return value.toLowerCase().replace(' ', '-') } } });
168
articleSchema.static({ list: function(callback){
this.find({}, null, {sort: {_id:-1}}, callback); }
})
module.exports = mongoose.model('Article', articleSchema);
The models/user.js file also begins with an inclusion and a schema:
var mongoose = require('mongoose'); var userSchema = new mongoose.Schema({ email: {
type: String, required: true,
set: function(value) {return value.trim().toLowerCase()}, validate: [ function(email) { return (email.match(/[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)* @(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i) != null)}, 'Invalid email' ] }, password: String, admin: { type: Boolean, default: false } });
module.exports = mongoose.model('User', userSchema);
The e-mail field is validated with RegExp, then is trimmed and forced to lowercase when it’s set.
The routes/article.js file now needs to switch to Mongoose models instead of Mongoskin collections. So, in the show method, this line goes away:
req.collections.articles.findOne({slug: req.params.slug}, function(error, article) {
Then, this line comes in:
req.models.Article.findOne({slug: req.params.slug}, function(error, article) {
In the list method, remove:
req.collections.articles.find({}).toArray(function(error, articles) {
and replace it with:
169
In the exports.add method,
req.collections.articles.insert( article,
function(error, articleResponse) {
is replaced with:
req.models.Article.create(article, function(error, articleResponse) {
The exports.edit method is trickier, and there are a few possible solutions:
1. Find a Mongoose document (e.g., findById) and use document methods (e.g., update) 2. Use the static model method findByIdAndUpdate
In both cases, this Mongoskin piece of code goes away:
req.collections.articles.updateById( req.params.id,
{$set: req.body.article}, function(error, count) {
We’ll use the former two-step approach (i.e., find then update), because it’s more versatile. So the above snippet is replaced by this code:
req.models.Article.findById( req.params.id,
function(error, article) { if (error) return next(error);
article.update({$set: req.body.article}, function(error, count, raw) { if (error) return next(error);
res.send({affectedCount: count}); })
});
Just to show you a more elegant one-step approach (the latter from the new exports.edit implementation list above):
req.models.Article.findByIdAndUpdate( req.params.id,
{$set: req.body.article}, function(error, doc) {
if (error) return next(error); res.send(doc);
} );
170
Similarly, with the exports.del request handler:
exports.del = function(req, res, next) {
if (!req.params.id) return next(new Error('No article ID.'));
req.models.Article.findById(req.params.id, function(error, article) { if (error) return next(error);
if (!article) return next(new Error('article not found')); article.remove(function(error, doc){
if (error) return next(error); res.send(doc);
}); }); };
The exports.postArticle and exports.admin functions look like these (the functions’ bodies are the same):
req.models.Article.create(article, function(error, articleResponse) { ...
req.models.Article.list(function(error, articles) { ...
Again, that’s all we have to do to switch to Mongoose for this route. However, to make sure there’s nothing missing, here’s the full code of the routes/article.js file:
exports.show = function(req, res, next) {
if (!req.params.slug) return next(new Error('No article slug.'));
req.models.Article.findOne({slug: req.params.slug}, function(error, article) { if (error) return next(error);
if (!article.published && !req.session.admin) return res.send(401); res.render('article', article);
}); };
exports.list = function(req, res, next) {
req.models.Article.list(function(error, articles) { if (error) return next(error);
res.send({articles: articles}); });
};
exports.add = function(req, res, next) {
if (!req.body.article) return next(new Error('No article payload.')); var article = req.body.article;
article.published = false;
req.models.Article.create(article, function(error, articleResponse) { if (error) return next(error);
res.send(articleResponse); });
171
exports.edit = function(req, res, next) {
if (!req.params.id) return next(new Error('No article ID.'));
req.models.Article.findById(req.params.id, function(error, article) { if (error) return next(error);
article.update({$set: req.body.article}, function(error, count, raw){ if (error) return next(error);
res.send({affectedCount: count}); })
}); };
exports.del = function(req, res, next) {
if (!req.params.id) return next(new Error('No article ID.'));
req.models.Article.findById(req.params.id, function(error, article) { if (error) return next(error);
if (!article) return next(new Error('article not found')); article.remove(function(error, doc){
if (error) return next(error); res.send(doc);
}); }); };
exports.post = function(req, res, next) { if (!req.body.title)
res.render('post'); };
exports.postArticle = function(req, res, next) {
if (!req.body.title || !req.body.slug || !req.body.text ) {
return res.render('post', {error: 'Fill title, slug and text.'}); } var article = { title: req.body.title, slug: req.body.slug, text: req.body.text, published: false };
req.models.Article.create(article, function(error, articleResponse) { if (error) return next(error);
res.render('post', {error: 'Article was added. Publish it on Admin page.'}); });
};
exports.admin = function(req, res, next) {
req.models.Article.list(function(error, articles) { if (error) return next(error);
res.render('admin',{articles:articles}); });
172
The routes/index.js file, which serves the home page, is as follows:
exports.article = require('./article'); exports.user = require('./user');
exports.index = function(req, res, next){
req.models.Article.find({published: true}, null, {sort: {_id:-1}}, function(error, articles){ if (error) return next(error);
res.render('index', { articles: articles}); })
};
Lastly, routes/user.js has a single line to change. Instead of:
req.collections.articles.find({}).toArray(function(error, articles) {
Now, we have:
...
req.models.User.findOne({ ...
To check if everything went well, simply run Blog as usual with $ node app and navigate the pages on
http://localhost:3000/. In addition, we can run Mocha tests with $ mocha test.
Summary
In this chapter, we learned what Mongoose is, how to install it, how to establish a connection to the database, and how to create Mongoose schemas while keeping the code organized with hooks and methods. We also compiled schemas into models and populated references automatically, and used virtual fields and custom schema type properties. Last, we refactored Blog to use Mongoose and made our app gain a true MVC architecture.
Next, we’ll cover how to build REST APIs with the two Node.js frameworks: Express.js and Hapi. This is an important topic, because more and more web developments shift towards heavy front-end logic and thin back- end. Some systems even go as far as building/using free-JSON APIs or back-as-a-service services. This tendency allows teams to focus on what is the most important for end-users: user interface, features, as well as what is vital for businesses: reduced iteration cycles, lower costs of maintenance and development.
Another essential piece in this puzzle is test-driven practice. To explore it, we’ll cover Mocha, which is a widely used Node.js testing framework. To REST APIs and TDD onward.