ExpressJS - Complete MVC

expressjs

http://code.tutsplus.com/tutorials/build-a-complete-mvc-website-with-expressjs--net-34168 - done reading
https://github.com/tutsplus/build-complete-website-expressjs/tree/master/app - done reading

app.js:

/**
 * Module dependencies.
 */

var express = require('express'),
    http = require('http'), 
    path = require('path'),
    config = require('./config')(),
    app = express(),
    MongoClient = require('mongodb').MongoClient,
    Admin = require('./controllers/Admin'),
    Home = require('./controllers/Home'),
    Blog = require('./controllers/Blog'),
    Page = require('./controllers/Page');

// all environments
// app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/templates');
app.set('view engine', 'hjs');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser('fast-delivery-site'));
app.use(express.session());
app.use(app.router);
app.use(require('less-middleware')({ src: __dirname + '/public' }));
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
      app.use(express.errorHandler());
}

MongoClient.connect('mongodb://' + config.mongo.host + ':' + config.mongo.port + '/fastdelivery', function(err, db) {
    if(err) {
        console.log('Sorry, there is no mongo db server running.');
    } else {
        var attachDB = function(req, res, next) {
            req.db = db;
            next();
        };
        app.all('/admin*', attachDB, function(req, res, next) {
            Admin.run(req, res, next);
        });            
        app.all('/blog/:id', attachDB, function(req, res, next) {
            Blog.runArticle(req, res, next);
        });    
        app.all('/blog', attachDB, function(req, res, next) {
            Blog.run(req, res, next);
        });    
        app.all('/services', attachDB, function(req, res, next) {
            Page.run('services', req, res, next);
        });    
        app.all('/careers', attachDB, function(req, res, next) {
            Page.run('careers', req, res, next);
        });    
        app.all('/contacts', attachDB, function(req, res, next) {
            Page.run('contacts', req, res, next);
        });    
        app.all('/', attachDB, function(req, res, next) {
            Home.run(req, res, next);
        });        
        http.createServer(app).listen(config.port, function() {
              console.log(
                  'Successfully connected to mongodb://' + config.mongo.host + ':' + config.mongo.port,
                  '\nExpress server listening on port ' + config.port
              );
        });
    }
});

config/index.js:

var config = {
    local: {
        mode: 'local',
        port: 3000,
        mongo: {
            host: '127.0.0.1',
            port: 27017
        }
    },
    staging: {
        mode: 'staging',
        port: 4000,
        mongo: {
            host: '127.0.0.1',
            port: 27017
        }
    },
    production: {
        mode: 'production',
        port: 5000,
        mongo: {
            host: '127.0.0.1',
            port: 27017
        }
    }
}
module.exports = function(mode) {
    return config[mode || process.argv[2] || 'local'] || config.local;
}

tests/config.spec.js:

describe("Configuration setup", function() {
    it("should load local configurations", function(next) {
        var config = require('../config')();
        expect(config.mode).toBeDefined();
        expect(config.mode).toBe('local');
        next();
    });
    it("should load staging configurations", function(next) {
        var config = require('../config')('staging');
        expect(config.mode).toBeDefined();
        expect(config.mode).toBe('staging');
        next();
    });
    it("should load production configurations", function(next) {
        var config = require('../config')('production');
        expect(config.mode).toBeDefined();
        expect(config.mode).toBe('production');
        next();
    });
});

controllers/Base.js:

var _ = require("underscore");
module.exports = {
    name: "base",
    extend: function(child) {
        return _.extend({}, this, child);
    },
    run: function(req, res, next) {

    }
}

controllers/Admin.js:

var BaseController = require("./Base"),
    View = require("../views/Base"),
    model = new (require("../models/ContentModel")),
    crypto = require("crypto"),
    fs = require("fs");

module.exports = BaseController.extend({ 
    name: "Admin",
    username: "admin",
    password: "admin",
    run: function(req, res, next) {
        var self = this;
        if(this.authorize(req)) {
            model.setDB(req.db);
            req.session.fastdelivery = true;
            req.session.save();
            var v = new View(res, 'admin');
            self.del(req, function() {
                self.form(req, res, function(formMarkup) {
                    self.list(function(listMarkup) {
                        v.render({
                            title: 'Administration',
                            content: 'Welcome to the control panel',
                            list: listMarkup,
                            form: formMarkup
                        });
                    });
                });
            });
        } else {
            var v = new View(res, 'admin-login');
            v.render({
                title: 'Please login'
            });
        }        
    },
    authorize: function(req) {
        return (
            req.session && 
            req.session.fastdelivery && 
            req.session.fastdelivery === true
        ) || (
            req.body && 
            req.body.username === this.username && 
            req.body.password === this.password
        );
    },
    list: function(callback) {
        model.getlist(function(err, records) {
            var markup = '<table>';
            markup += '\
                <tr>\
                    <td><strong>type</strong></td>\
                    <td><strong>title</strong></td>\
                    <td><strong>picture</strong></td>\
                    <td><strong>actions</strong></td>\
                </tr>\
            ';
            for(var i=0; record = records[i]; i++) {
                markup += '\
                <tr>\
                    <td>' + record.type + '</td>\
                    <td>' + record.title + '</td>\
                    <td><img class="list-picture" src="' + record.picture + '" /></td>\
                    <td>\
                        <a href="/admin?action=delete&id=' + record.ID + '">delete</a>&nbsp;&nbsp;\
                        <a href="/admin?action=edit&id=' + record.ID + '">edit</a>\
                    </td>\
                </tr>\
            ';
            }
            markup += '</table>';
            callback(markup);
        })
    },
    form: function(req, res, callback) {
        var returnTheForm = function() {
            if(req.query && req.query.action === "edit" && req.query.id) {
                model.getlist(function(err, records) {
                    if(records.length > 0) {
                        var record = records[0];
                        res.render('admin-record', {
                            ID: record.ID,
                            text: record.text,
                            title: record.title,
                            type: '<option value="' + record.type + '">' + record.type + '</option>',
                            picture: record.picture,
                            pictureTag: record.picture != '' ? '<img class="list-picture" src="' + record.picture + '" />' : ''
                        }, function(err, html) {
                            callback(html);
                        });
                    } else {
                        res.render('admin-record', {}, function(err, html) {
                            callback(html);
                        });
                    }
                }, {ID: req.query.id});
            } else {
                res.render('admin-record', {}, function(err, html) {
                    callback(html);
                });
            }
        }
        if(req.body && req.body.formsubmitted && req.body.formsubmitted === 'yes') {
            var data = {
                title: req.body.title,
                text: req.body.text,
                type: req.body.type,
                picture: this.handleFileUpload(req),
                ID: req.body.ID
            }
            model[req.body.ID != '' ? 'update' : 'insert'](data, function(err, objects) {
                returnTheForm();
            });
        } else {
            returnTheForm();
        }
    },
    del: function(req, callback) {
        if(req.query && req.query.action === "delete" && req.query.id) {
            model.remove(req.query.id, callback);
        } else {
            callback();
        }
    },
    handleFileUpload: function(req) {
        if(!req.files || !req.files.picture || !req.files.picture.name) {
            return req.body.currentPicture || '';
        }
        var data = fs.readFileSync(req.files.picture.path);
        var fileName = req.files.picture.name;
        var uid = crypto.randomBytes(10).toString('hex');
        var dir = __dirname + "/../public/uploads/" + uid;
        fs.mkdirSync(dir, '0777');
        fs.writeFileSync(dir + "/" + fileName, data);
        return '/uploads/' + uid + "/" + fileName;
    }
});

controllers/Blog.js:

var BaseController = require("./Base"),
    View = require("../views/Base"),
    model = new (require("../models/ContentModel"));

module.exports = BaseController.extend({ 
    name: "Blog",
    content: null,
    run: function(req, res, next) {
        model.setDB(req.db);
        var self = this;
        this.getContent(function() {
            var v = new View(res, 'blog');
            v.render(self.content);
        });
    },
    runArticle: function(req, res, next) {
        model.setDB(req.db);
        var self = this;
        this.getArticle(req.params.id, function() {
            var v = new View(res, 'inner');
            v.render(self.content);
        });
    },
    getContent: function(callback) {
        var self = this;
        this.content = {};
            model.getlist(function(err, records) {
                var blogArticles = '';
                if(records.length > 0) {
                    for(var i=0; record=records[i]; i++) {
                        var record = records[i];
                        blogArticles += '\
                            <section class="item">\
                                <img src="' + record.picture + '" alt="" />\
                                <h2>' + record.title + '</h2>\
                                <p>' + record.text + '</p>\
                                <br class="clear" />\
                                <hr />\
                            </section>\
                        ';
                    }
                }
                self.content.blogArticles = blogArticles;
                callback();
        }, { type: 'blog' });
    },
    getArticle: function(ID, callback) {
        var self = this;
        this.content = {}
        model.getlist(function(err, records) {
            if(records.length > 0) {
                self.content = records[0];
            }
            callback();
        }, { ID: ID });
    }
});

controllers/Home.js:

var BaseController = require("./Base"),
    View = require("../views/Base"),
    model = new (require("../models/ContentModel"));

module.exports = BaseController.extend({ 
    name: "Home",
    content: null,
    run: function(req, res, next) {
        model.setDB(req.db);
        var self = this;
        this.getContent(function() {
            var v = new View(res, 'home');
            v.render(self.content);
        })
    },
    getContent: function(callback) {
        var self = this;
        this.content = {};
        model.getlist(function(err, records) {
            if(records.length > 0) {
                self.content.bannerTitle = records[0].title;
                self.content.bannerText = records[0].text;
            }
            model.getlist(function(err, records) {
                var blogArticles = '';
                if(records.length > 0) {
                    var to = records.length < 5 ? records.length : 4;
                    for(var i=0; i<to; i++) {
                        var record = records[i];
                        blogArticles += '\
                            <div class="item">\
                                <img src="' + record.picture + '" alt="" />\
                                <a href="/blog/' + record.ID + '">' + record.title + '</a>\
                            </div>\
                        ';
                    }
                }
                self.content.blogArticles = blogArticles;
                callback();
            }, { type: 'blog' });
        }, { type: 'home' });
    }
});

controllers/Page.js:

ar BaseController = require("./Base"),
    View = require("../views/Base"),
    model = new (require("../models/ContentModel"));

module.exports = BaseController.extend({ 
    name: "Page",
    content: null,
    run: function(type, req, res, next) {
        model.setDB(req.db);
        var self = this;
        this.getContent(type, function() {
            var v = new View(res, 'inner');
            v.render(self.content);
        });
    },
    getContent: function(type, callback) {
        var self = this;
        this.content = {}
        model.getlist(function(err, records) {
            if(records.length > 0) {
                self.content = records[0];
            }
            callback();
        }, { type: type });
    }
});

models/Base.js:

module.exports = function(db) {
    this.db = db;
};
module.exports.prototype = {
    extend: function(properties) {
        var Child = module.exports;
        Child.prototype = module.exports.prototype;
        for(var key in properties) {
            Child.prototype[key] = properties[key];
        }
        return Child;
    },
    setDB: function(db) {
        this.db = db;
    },
    collection: function() {
        if(this._collection) return this._collection;
        return this._collection = this.db.collection('fastdelivery-content');
    }
}

models/ContentModel.js:

var Model = require("./Base"),
    crypto = require("crypto"),
    model = new Model();
var ContentModel = model.extend({
    insert: function(data, callback) {
        data.ID = crypto.randomBytes(20).toString('hex'); 
        this.collection().insert(data, {}, callback || function(){ });
    },
    update: function(data, callback) {
        this.collection().update({ID: data.ID}, data, {}, callback || function(){ });    
    },
    getlist: function(callback, query) {
        this.collection().find(query || {}).toArray(callback);
    },
    remove: function(ID, callback) {
        this.collection().findAndModify({ID: ID}, [], {}, {remove: true}, callback);
    }
});
module.exports = ContentModel;

templates/admin-login.hjs:

<!DOCTYPE html>
<html>
    <head>
        <title>{{ title }}</title>
        <link rel='stylesheet' href='/stylesheets/style.css' />
    </head>
    <body class="admin">
        <div class="container">
            <form action="/admin" method="post">
                Username:<br />
                <input type="text" name="username" /><br />
                Password:<br />
                <input type="password" name="password" /><br /><br />
                <input type="submit" value="login" />
            </form>
        </div>
    </body>
</html>

templates/admin-record.hjs:

<form action="/admin" method="post" enctype="multipart/form-data">
    <input type="hidden" name="formsubmitted" value="yes">
    <input type="hidden" name="currentPicture" value="{{picture}}">
    <input type="hidden" name="ID" value="{{ID}}">
    Title:<br />
    <input type="text" name="title" value="{{title}}" /><br />
    Text:<br />
    <textarea name="text">{{text}}</textarea><br />
    Picture:<br />
    <input type="file" name="picture" /><br />{{{pictureTag}}}<br />
    Type:<br />
    <select name="type">
        {{{type}}
        <option value="blog">blog</option>
        <option value="home">home</option>
        <option value="services">services</option>
        <option value="careers">careers</option>
        <option value="contacts">contacts</option>
    </select><br /><br />
    <input type="submit" value="submit" />
</form>

templates/admin.hjs:

<!DOCTYPE html>
<html>
    <head>
        <title>{{title}}</title>
        <link rel='stylesheet' href='/stylesheets/style.css' />
    </head>
    <body class="admin">
        <div class="container">
            <h1>{{content}}</h1>
            <hr />
            <div class="left list-column">
                {{{list}}}
            </div>
            <div class="left form-column">
                {{{form}}}
            </div>
            <br class="clear" />
            <hr />
        </div>
    </body>
</html>

templates/blog.hjs:

<!DOCTYPE html>
<html>
    <head>
        <title>{{ title }}</title>
        <link rel='stylesheet' href='/stylesheets/style.css' />
    </head>
    <body>
        <div class="container">

            <!-- navigation -->
            <nav>
                <div class="content">
                    <a href="/" class="logo">
                        <img src="/images/logo.png" alt="FastDelivery">
                    </a>
                    <ul class="menu">
                        <li><a href="/">Home</a></li>
                        <li><a href="/blog">Blog</a></li>
                        <li><a href="/services">Services</a></li>
                        <li><a href="/careers">Careers</a></li>
                        <li><a href="/contacts">Contacts</a></li>
                    </ul>
                    <br class="clear" />
                </div>
            </nav>

            <!-- inner page -->
            <div class="inner">
                <div class="content">
                    {{{blogArticles}}}
                </div>
            </div>

            <!-- footer -->
            <div class="footer">
                <div class="content">
                    fast delivery &copy; 2013
                </div>
            </div>

        </div>
    </body>
</html>

templates/home.hjs:

<!DOCTYPE html>
<html>
    <head>
        <title>{{ title }}</title>
        <link rel='stylesheet' href='/stylesheets/style.css' />
    </head>
    <body>
        <div class="container">

            <!-- navigation -->
            <nav>
                <div class="content">
                    <a href="/" class="logo">
                        <img src="/images/logo.png" alt="FastDelivery">
                    </a>
                    <ul class="menu">
                        <li><a href="/">Home</a></li>
                        <li><a href="/blog">Blog</a></li>
                        <li><a href="/services">Services</a></li>
                        <li><a href="/careers">Careers</a></li>
                        <li><a href="/contacts">Contacts</a></li>
                    </ul>
                    <br class="clear" />
                </div>
            </nav>

            <!-- home page -->
            <div class="home">
                <h1>Fast As Hell <small>The fastest-growing company of 2012</small></h1>
                <div class="teaser">
                    <div class="content">
                        <img src="/images/home-teaser-image.jpg" alt="" class="home-teaser-image" />
                        <h2>{{bannerTitle}}</h2>
                        <p>{{bannerText}}</p>
                        <div class="home-teaser-icons">
                            <a href="#"><img src="/images/home-teaser-icon1.png" alt=""/></a>
                            <a href="#"><img src="/images/home-teaser-icon2.png" alt=""/></a>
                            <a href="#"><img src="/images/home-teaser-icon3.png" alt=""/></a>
                            <a href="#"><img src="/images/home-teaser-icon4.png" alt=""/></a>
                            <br class="clear" />
                        </div>
                    </div>
                </div>
                <div class="blog-articles">
                    <div class="content">
                        {{{blogArticles}}}
                    </div>
                    <br class="clear" />
                </div>
            </div>

            <!-- footer -->
            <div class="footer">
                <div class="content">
                    fast delivery &copy; 2013
                </div>
            </div>

        </div>
    </body>
</html>

templates/inner.hjs:

<!DOCTYPE html>
<html>
    <head>
        <title>{{ title }}</title>
        <link rel='stylesheet' href='/stylesheets/style.css' />
    </head>
    <body>
        <div class="container">

            <!-- navigation -->
            <nav>
                <div class="content">
                    <a href="/" class="logo">
                        <img src="/images/logo.png" alt="FastDelivery">
                    </a>
                    <ul class="menu">
                        <li><a href="/">Home</a></li>
                        <li><a href="/blog">Blog</a></li>
                        <li><a href="/services">Services</a></li>
                        <li><a href="/careers">Careers</a></li>
                        <li><a href="/contacts">Contacts</a></li>
                    </ul>
                    <br class="clear" />
                </div>
            </nav>

            <!-- inner page -->
            <div class="inner">
                <div class="content">
                    <section>
                        <img src="{{picture}}" alt="" />
                        <h1>{{title}}</h1>
                        <p>{{text}}</p>
                    </section>
                </div>
            </div>

            <!-- footer -->
            <div class="footer">
                <div class="content">
                    fast delivery &copy; 2013
                </div>
            </div>

        </div>
    </body>
</html>

views/Base.js:

module.exports = function(response, template) {
    this.response = response;
    this.template = template;
};
module.exports.prototype = {
    extend: function(properties) {
        var Child = module.exports;
        Child.prototype = module.exports.prototype;
        for(var key in properties) {
            Child.prototype[key] = properties[key];
        }
        return Child;
    },
    render: function(data) {
        if(this.response && this.template) {
            this.response.render(this.template, data);
        }
    }
}

tests/base.controller.spec.js:

var BaseController = require("../controllers/Base");
describe("Base controller", function() {
    it("should have a method extend which returns a child instance", function(next) {
        expect(BaseController.extend).toBeDefined();
        var child = BaseController.extend({ name: "my child controller" });
        expect(child.run).toBeDefined();
        expect(child.name).toBeDefined();
        expect(child.name).toBe("my child controller");
        next();
    });
    it("should be able to create different childs", function(next) {
        var childA = BaseController.extend({ name: "child A", customProperty: 'value' });
        var childB = BaseController.extend({ name: "child B" });
        expect(childA.name).not.toBe(childB.name);
        expect(childB.customProperty).not.toBeDefined();
        next();
    });
});

tests/base.model.spec.js:

var Model = require("../models/Base"),
    dbMockup = {};
describe("Models", function() {
    it("should create a new model", function(next) {
        var model = new Model(dbMockup);
        expect(model.db).toBeDefined();
        expect(model.extend).toBeDefined();
        next();
    });
    it("should be extendable", function(next) {
        var model = new Model(dbMockup);
        var OtherTypeOfModel = model.extend({
            myCustomModelMethod: function() { }
        });
        var model2 = new OtherTypeOfModel(dbMockup);
        expect(model2.db).toBeDefined();
        expect(model2.myCustomModelMethod).toBeDefined();
        next();
    })
});

tests/base.view.spec.js:

var View = require("../views/Base");
describe("Base view", function() {
    it("create and render new view", function(next) {
        var responseMockup = {
            render: function(template, data) {
                expect(data.myProperty).toBeDefined();
                expect(data.myProperty).toBe('value');
                expect(template).toBe('template-file');
                next();
            }
        }
        var v = new View(responseMockup, 'template-file');
        v.render({myProperty: 'value'});
    });
    it("should be extendable", function(next) {
        var v = new View();
        var OtherView = v.extend({
            render: function(data) {
                expect(data.prop).toBeDefined();
                expect(data.prop).toBe('yes');
                next();
            }
        });
        var otherViewInstance = new OtherView();
        expect(otherViewInstance.render).toBeDefined();
        otherViewInstance.render({prop: 'yes'});
    });
});

tests/mongodb.spec.js:

describe("MongoDB", function() {
    it("is there a server running", function(next) {
        var MongoClient = require('mongodb').MongoClient;
        MongoClient.connect('mongodb://127.0.0.1:27017/fastdelivery', function(err, db) {
            expect(err).toBe(null);
            expect(db).toBeDefined();
            next();
        });
    });
});
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License