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.
Pingback: Testing a Node.js Express API server with Vows (functional) « Fabiano PS
How about authorization?
Made an update about it.
Nice stuff.. Really helpful!!
Great post! I recreated the app and got an error though:
> api2@0.0.1 start /Volumes/Terradisk/Git/api2
> nodemon api.js
23 Nov 17:24:12 – [nodemon] v0.6.23
23 Nov 17:24:12 – [nodemon] watching: /Volumes/Terradisk/Git/api2
23 Nov 17:24:12 – [nodemon] starting `node api.js`
/Volumes/Terradisk/Git/api2/routes.js:86
app.resource(‘admin/users’, require(‘./resources/users’));
^
TypeError: Object function app(req, res){ app.handle(req, res); } has no method ‘resource’
at Object. (/Volumes/Terradisk/Git/api2/routes.js:86:5)
at Module._compile (module.js:449:26)
at Object.Module._extensions..js (module.js:467:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Module.require (module.js:362:17)
at require (module.js:378:17)
at Object. (/Volumes/Terradisk/Git/api2/api.js:69:1)
at Module._compile (module.js:449:26)
at Object.Module._extensions..js (module.js:467:10)
I see, probably some APIs changed, I am sorry I cannot support you on this one. Usually what I do is, first to check on the docs, and after to load relevant code parts in node interactive mode and see how things behave. Debugger tools are also pretty cool.
Got it working. Perhaps once I’m a bit further with it, I could make it publicly available on Github and you can share the link in the post, so others can enjoy it and it may turn into an even nicer library?
Hi sander, absolutely, please go ahead. I can link your repo to the top of the post, so people can checkout your newest version, I’d love it, since I don’t really have conditions to keep up with it’s maintenance nowadays, at least I know people are getting code up-to-date
Reblogged this on joederrigan and commented:
Wow! Really cool… I was looking for an example of how all the edge technologies fit together. This one is a demonstration of Expressjs Framework, Node.js and Sequelize. Especially since Amazon AWS now supports Node.js services for the Beanstalk. – https://forums.aws.amazon.com/ann.jspa?annID=1884