deck-node

A web application framework for Node.

npm install deck-node
70 downloads in the last week
129 downloads in the last month

Deck - you hold the cards

Deck is a web application framework for Node.

Some features:

  • Simple promise based Postgres client and ORM
  • Dependency injection
  • Beautiful Connect compatible RESTful routing, including clean URLs, route groups, filters, etc
  • Helpful middleware such as res.redirect(), res.json() or res.view()
  • Easy and flexible session handling, including one time messages

Deck is free to use and available under the MIT license.

I try to test every Deck component thouroughly. I would say it's about 98% unit tested. Here's the test report for the current build:

Build Status

Todo

  • Optionally check user-agent to verify the session cookie
  • Test the Session middleware
  • Test the Postgres session store
  • Add Lang class to deck-node
  • Domain / subdomain support on the Router
  • Test Client.prototype.end()

Code

The following examples showcase the syntax and philosophy behind the Deck framework. They have not been tested and may include insecure coding practices for the sake of readability.

Routing

// routes.js

var users = require('./controllers/users');
var filters = require('./filters');

module.exports = function(router) {
    /*
     * The `{lang}` makes `req.params.lang` available in the
     * controllers. The call to `action()` adds the specified
     * middleware to all grouped routes.
     */
    router.prefix('/{lang}').action(filters.ipBans, function() {

        router.get('/users').action(users.all);
        router.get('/users/{id}').action(users.byId);
        router.post('/users').action(users.create);

    });
}

Session

Deck comes with a custom session Handler. The Store used to persist the session data across requests is injected into the Handler so you can build your own. For instance, if you want to save session data in Mongo, just build an adapter modeled after the MemoryStore - available for testing - and you should be good to go!

// controllers/some_controller.js

exports.session = function(req, res) {
    // csrf token
    req.session.token();

    // session id
    req.session.id();

    if (req.query.logout) {
        req.session.flush();
        req.session.push('messages', 'You have successfully logged out.');

        // makes messages available only for the next request
        req.session.flash('messages');
        res.redirect('/');
    }

    res.view('session.html', {
        username: req.session.get('username') || 'user'
    });

    req.session.set('username', 'Bob');
}

Controllers

// controllers/users.js

exports.all = function(req, res, next, app) {
    var User = app.get('models').User;

    User.fetch('SELECT * FROM users')
        .then(function(users) {
            res.json(users);
        })
        .catch(function(e) {
            next(e);
        });
}

exports.byId = function(req, res, next, app) {
    var User = app.get('models').User;

    User.find(req.params.id)
        .then(function(user) {
            res.json(user);
        })
        .catch(function(e) {
            next(e);
        });
}

exports.create = function(req, res, next, app) {
    var User = app.get('models').User;

    new User(req.body).save()
        .then(function(user) {
            res.json({ user: user });
        })
        .catch(User.ValidationError, function(e) {
            // Some of the given values were not right
            res.json(500, { errors: e.toArray() });
        })
        .catch(function(e) {
            next(e);
        });
}
// filters.js

var _ = require('underscore');

/*
 * If the client is in the banned IP list, return with a 403.
 */
exports.ipBans = function(req, res, next) {
    var ips = [ 'bad-guy', 'other-bad-guy' ];
    if (_.contains(ips, req.socket.remoteAddress)) {
        res.statusCode = 403;
        return res.end("You've been banned!");
    }
    next();
}

ORM / Models

The ORM in Deck is a mix of SQL and ORM. Basically, when you do something like User.fetch('SELECT * FROM users'), the result of the SQL query will populate the User model. That gives you the flexibility of SQL with the benefits from object modeling, like data validation, automatic types conversions from/to the database, etc.

// models/user.js

var Model = Deck.Database.Postgres.Model;

module.exports = User;

Model.extend(User);
function User(values) {
    this.set(values);
}

Users.withNumPosts = function() {
    var sql = '';
    sql += 'SELECT *, '
    sql += '(SELECT COUNT(*) FROM posts p WHERE p.uid = u.id) AS num_posts ';
    sql += 'FROM users u LIMIT 1';

    // Return the promise for that user
    return this.fetchOne(sql);
}

/*
 * Postgres returns `COUNT(*)` as a BigInt, so a string, but we
 * want an integer. When calling `user.set('numPosts', '14')`, the
 * custom setter below will be called automatically so the `'14'`
 * will instead be saved as `14`.
 */
User.prototype.setNumPosts = function(val) {
    this.values['numPosts'] = parseInt(val);
    return this;
}

});

Cross Site Request Forgery

Deck comes with a single CSRF middleware. Here's how to enable CSRF protection in your app:

// app.js

var Deck = require('deck-node');
var app = new Deck.App('dev');
app.use(app.middleware.session(new Deck.Session.Store.Memory()));
app.use(app.middleware.csrf());
// controllers/login.js

exports.showForm = function(req, res) {
    res.view('login_form.html', {
        token: req.session.token()
    });
}
// views/login_form.html

<form action="/login" method="POST">
    <input type="hidden" name="_csrf_token" value="<%= token %>">
    <input type="text" name="username" placeholder="Username">
    <input type="password" name="password" placeholder="*******">
</form>

The application runner

// app.js

var Deck = require('deck-node');
var app = new Deck.App('dev');

// configure dependency injection container
var di = require('./di.js');
di(app.container);

app.use(require('connect').json());
app.use(app.middleware.json()); // res.json(...);
app.use(app.middleware.router(require('./routes'), app.container));

app.listen(3000).then(function() {
    console.log('Listening on port 3000 :-)');
});

Dependency injection

The dependency

// di.js

var Deck = require('deck-node');

module.exports = function(container) {

    container.instance('db', new Deck.Database.Postgres.Client({
            user: 'dba',
            password: 'secret',
            database: 'myproject'
        })
    );

    container.singleton('models', function() {
        var User = require('./models/user');

        User.dbClient = container.get(Deck.Database.Postgres.Client);

        return {
            User: User
        };
    });

    // Each call to `container.get('Superman')` will return
    // a new instance of the Superman user.
    container.bind('Superman', function() {
        var User = container.get('models').User;

        return new User({
            username: 'Superman',
            email: 'superman@krypton.pl'
        });
    });

}
npm loves you