via Axiom Zen
Hello and welcome!
Foreword, and purpose
I will be putting this JSON-API up, to serve USERS as a restful resource, trying to be light as possible, still serving content only to authenticated peers (auth based on cookie).
The regular use will be something as, the client makes login, sending params via POST, ant then is able to access all resources we may serve.
I will be using some packages, all being possible to install via npm install, lets talk about them:
- Express, itself is like the Ruby’s Sinatra Framework, but async, for Node.js
- Express-resource, a simple and extendable way to define Restful resources
- Sequelize, a MySQL ORM! Offer n-m relations, and a .sync() way to define your own Schema! (better than migrations?)
- Faker, a generator of fake-data that we need
- Mustache, String template engine. Will be using for minimal tasks, not required.
- Jade, view template engine. Not required at all, but since I started coding there, will keep it for now.
The app skeleton
Install express with -g and generate an app from it.
We will be keeping 3 core files:
app.js : The main file; require all other from there, and configure Express!
models.js : The file that has all models definitions exported.
routes.js : Acts as a general router and controller for the app. Also holds the functionality of authenticating all users that get past /admin/*. The authorization method here is static, in this case, one e-mail bounded to one password. See Auth Methods app.post(‘/login’.. for details.
Files
./app.js
// Detect Params http://nodejs.org/docs/latest/api/process.html#process.argv
if (process.argv.indexOf('--seed') > -1) {
/**
* Indicates when the DB should be re-schemed and seeded again
*/
GLOBAL.db_seed = true;
}
if (process.argv.indexOf('--no-auth') > -1 ){
/**
* Indicates when to run without requiring auth for API
*/
GLOBAL.no_auth = true;
}
var express = require('express'),
/**
* Instantiate the unique Express instance
*/
app = module.exports = express.createServer(),
models = require('./models.js'),
S = require('mustache').to_html;
/**
* @type {Object}
*
* A list of all Sequelize Models available, representing the tables.
*/
GLOBAL.models = models;
/**
* @type {Express}
*
* The Singleton of Express app instance
*/
GLOBAL.app = app;
// Configuration
app.configure(function() {
app.use(express.logger({format: 'dev'}));
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.bodyParser());
app.use(express.cookieParser());
app.use(express.session({ secret: 'evilWorldDom1nat10nPlanzisstillsmallshouldhaveNoWords' }));
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
app.configure('development', function() {
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
app.configure('production', function() {
app.use(express.errorHandler());
});
// Routes
require('./routes.js');
app.listen(8010);
console.log('Express server listening on port %d in %s mode',
app.address().port, app.settings.env);
./models.js
var Sequelize = require('sequelize'),
db = new Sequelize('dev_', 'root', '123456');
/**
* @type {Object}
* Map all attributes of the registry
* (Instance method useful to every sequelize Table)
* @this {SequelizeRegistry}
* @return {Object} All attributes in a Object.
*/
var map_attributes = function() {
var obj = new Object(),
ctx = this;
ctx.attributes.forEach(
function(attr) {
obj[attr] = ctx[attr];
}
);
return obj;
};
/**
* @type {Object}
* All models we have defined over Sequelize, plus the db instance itself
*/
var self = module.exports = {
'db' : db,
User: db.define('user',
{
name: {
type: Sequelize.STRING,
defaultValue: 'Not Big Deal',
allowNull: false
}
},
{
timestamps: true,
freezeTableName: true,
classMethods: {
staticExample: function() { this.name }
},
instanceMethods: {
mapAttributes: map_attributes
}
}
),
Activity: db.define('activity',
{
type: { type: Sequelize.STRING },
value: { type: Sequelize.STRING }
},
{
timestamps: true,
freezeTableName: true,
instanceMethods: {
mapAttributes: map_attributes
}
}
),
Company: db.define('company',
{
name: { type: Sequelize.STRING, defaultValue: 'MegaCorp', allowNull: false }
},
{
timestamps: true,
freezeTableName: true,
instanceMethods: {
mapAttributes: map_attributes
}
})
};
self.Activity.belongsTo(self.User, {foreignKey: 'user_id'});
self.User.hasMany(self.Activity, {foreignKey: 'user_id'});
self.User.belongsTo(self.Company, {foreignKey: 'company_id'});
self.Company.hasMany(self.User, {foreignKey: 'company_id'});
if (GLOBAL.db_seed) {
console.info('SEED TIME!');
var chainer = new Sequelize.Utils.QueryChainer;
chainer
.add(self.Company.sync({force: true}))
.add(self.User.sync({force: true}))
.add(self.Activity.sync({force: true}))
.run()
.on('success', function() {
var seeds = require('./seeds');
for (var model_name in seeds) {
console.log('MODEL', model_name);
for (var i = 0; i < seeds[model_name].length; i++) {
self[model_name].create(seeds[model_name][i])
.on('success', function(novo) { })
.on('failure', function(erro) {
console.error('DB SEED ERROR!!', erro);
process.kill();
});
console.log(' ', seeds[model_name][i]);
}
}
})
.on('failure', function(errors) {
console.error('DB schema change FAIL', errors);
process.kill();
});
}
// User.staticExample()
// User.build({}).instanceExample()
./routes.js
S = require('mustache').to_html;
Resource = require('express-resource'); // paths Express, add .resource
/**
* ROOT
*/
app.get('/', function(req, res) {
// This big mess just points out all routes we have, along with the verbs
var path_list = S(
'{{#routes_obj}}' +
"<a href='{{path}}'>{{method}}: {{path}} </a> <br/>" +
'{{/routes_obj}}',
{
routes_obj: (app.routes.routes.get ? app.routes.routes.get : [])
.concat((app.routes.routes.post ? app.routes.routes.post : [])
.concat((app.routes.routes.put ? app.routes.routes.put : [])
.concat(app.routes.routes.delete ? app.routes.routes.delete : [])
.concat([{ path: '/auth.html', method: 'get'}])))
}
);
res.render('index', {
title: 'DBServer',
'path_list': path_list,
logged: req.session.authed ? 'YES :)' : 'NO :('
});
});
/**
* AUTH methods
*/
app.post('/login', function(req, res) {
console.info('login PARAM: ', req.body);
var credentials = req.body.user;
if (GLOBAL.no_auth ||
(credentials &&
credentials.username == 'super_safe' &&
credentials.password == '123456')
) {
req.session.authed = true;
res.json(['OK']);
} else {
res.json(['FAIL']);
}
});
app.get('/logout', function(req, res) {
req.session.authed = null;
res.json(['OK']);
});
/*
* API Authentication filter
*/
app.all('/admin*', function(req, res, next) {
if (req.session.authed) {
next();
} else {
res.json(['must auth']);
}
});
/*
* Resources
*/
app.resource('admin/users', require('./resources/users'));
/**
* Just say it is all fine.
*/
module.exports = true;
./seeds.js
// https://github.com/marak/Faker.js/
// npm install Faker
var Faker = require('Faker'),
times = 0,
id = 0
var self = {
Company: [ ],
User: [ ],
Activity: [ ],
}
// Company: create 3 - 5
id = 1
times = Faker.Helpers.randomNumber(3)+3
for (; times > 0 ; times--) {
var novo = {
id: id++,
name: Faker.Company.companyName(),
}
self.Company.push( novo )
//console.log("Company", novo)
};
// Users: create 0 - 8 per company
id = 1
for (var i=0; i < self.Company.length; i++) {
times = Faker.Helpers.randomNumber(9)
for (; times > 0 ; times--) {
var novo = {
id: id++,
name: Faker.Name.firstName(),
company_id: Faker.Helpers.randomize( self.Company ).id,
}
self.User.push( novo )
//console.log("User", novo)
};
};
// Activities: create 0 - 10 per user
var TYPES = ['level_up', 'timeout' ],
VALUES = ['win', 'fail']
id = 1
for (var i=0; i < self.User.length; i++) {
times = Faker.Helpers.randomNumber(11)
for (; times > 0 ; times--) {
var novo = {
id: id++,
type: Faker.Helpers.randomize( TYPES ),
value: Faker.Helpers.randomize( VALUES ),
user_id: Faker.Helpers.randomize( self.User ).id,
}
self.Activity.push( novo )
//console.log("Activity", novo)
};
}
module.exports = self
./resources/users.js
exports.index = function(req, res) {
models.User.findAll().on('success', function(users) {
res.json(
users.map(function(user) {
return user.mapAttributes();
})
);
});
};
./views/index.jade
h1= title
p Welcome to #{title}
p Paths we have:
div!= path_list
div .
footer Loggedin? #{ logged }
./public/auth.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>auth</title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<div>
<header>
<h1>Auth</h1>
</header>
<nav>
<p>
<a href="/">Home</a>
</p>
</nav>
<div>
<form action="/login" method="post">
<label>Username: <input name="user[username]" type="text" /></label></br>
<label>Password: <input name="user[password]" type="password" /></label></br>
<button type="submit">OK</button>
</form>
</div>
<footer>
<p>
Express API
</p>
</footer>
</div>
</body>
</html>
Brief Discussion
Overall, this post presents a short and extensible way to start a API that requires authentication, via cookie, and returns data in JSON format.