Starting a Node.js Express API to serve json, (with auth, MySQL – based)

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.

About these ads

9 thoughts on “Starting a Node.js Express API to serve json, (with auth, MySQL – based)

  1. Pingback: Testing a Node.js Express API server with Vows (functional) « Fabiano PS

    • 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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s