Created
August 27, 2011 10:13
-
-
Save naholyr/1175210 to your computer and use it in GitHub Desktop.
REST avec NodeJS & Express - Application
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var express = require('express') | |
, app = module.exports = express.createServer() | |
app.configure(function () { | |
app.set('views', __dirname + '/views'); | |
app.set('view engine', 'jade'); | |
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()); | |
}); | |
// Montage de l'API REST sur /bookmarks | |
app.use('/bookmarks', app.bookmarks_app = require('./bookmarks-rest')()); | |
// Homepage | |
app.get('/', function (req, res) { | |
res.render('index', { "title": 'Bookmarks' }); | |
}); | |
if (module.parent === null) { | |
app.listen(3000); | |
console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var express = require('express') | |
, m = require('./middleware') | |
// Instanciated module | |
module.exports = function () { | |
var app = express.createServer(); | |
app.db = require('./db')(); | |
app.on('close', app.db.close); | |
app.configure(function () { | |
app.param('id', m.checkIdParameter); | |
app.use(m.checkRequestHeaders); | |
app.use(express.bodyParser()); | |
app.use(m.handleBodyParserError); | |
app.use(m.checkRequestData); | |
app.use(express.methodOverride()); | |
app.use(app.router); | |
}); | |
app.configure('development', function () { | |
app.use(m.errorHandler({"stack": true})); | |
}); | |
app.configure('production', function () { | |
app.use(m.errorHandler()); | |
}); | |
app.configure('test', function () { | |
app.use(m.errorHandler({"stack": false, "log": function showNothing(){}}); | |
}); | |
app.post('/', m.dbAction(db, 'save')); | |
app.get( '/', m.dbAction(db, 'fetchAll', function (ids) { return ids.map(function (id) { | |
// URIs depend on mount route | |
return app.route + (app.route.charAt(app.route.length-1) == '/' ? '' : '/') + 'bookmark/' + id; }); })); | |
app.get( '/bookmark/:id', m.dbAction(db, 'fetchOne')); | |
app.put( '/bookmark/:id', m.dbAction(db, 'save')); | |
app.del( '/', m.dbAction(db, 'deleteAll')); | |
app.del( '/bookmark/:id', m.dbAction(db, 'deleteOne')); | |
app.all( '/*', function (req, res, next) { next({"code":405, "message":"Method not allowed"}); }); | |
return app; | |
} | |
// Expose dependencies to avoid duplicate modules | |
exports.express = express; | |
exports.middlewares = m; | |
// Start when main module | |
if (module.parent == null) module.exports().listen(3000); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module.exports = function (options) { | |
/** | |
* Module options | |
*/ | |
var client = require('redis').createClient() | |
, namespace = 'bookmarks'; | |
if ('undefined' != typeof options) _set_options_(options) | |
/** | |
* Privates | |
*/ | |
// Get bookmark key name | |
function _key_ (id) { | |
return namespace + ':' + id + ':json'; | |
} | |
// Get sequence key name | |
function _seq_ () { | |
return namespace + '::sequence'; | |
} | |
// Update internal options | |
function _set_options_ (options) { | |
if ('undefined' != typeof options.database) client.select(options.database); | |
if ('undefined' != typeof options.namespace) namespace = options.namespace; | |
return this; | |
} | |
return { | |
/** | |
* Update options | |
*/ | |
"configure": _set_options_, | |
/** | |
* Allow disconnection | |
*/ | |
"close": function disconnect (callback) { | |
if (client.connected) client.quit(); | |
if (callback) client.on('close', callback); | |
}, | |
/** | |
* Save a bookmark | |
* if bookmark has no attribute "id", it's an insertion, else it's an update | |
* callback is called with (err, bookmark, created) | |
*/ | |
"save": function save (bookmark, callback) { | |
var created = ('undefined' == typeof bookmark.id); | |
var self = this; | |
var onIdReady = function () { | |
client.set(_key_(bookmark.id), JSON.stringify(bookmark), function (err) { | |
callback(err, bookmark, created); | |
}); | |
} | |
if (created) { // No ID: generate one | |
client.incr(_seq_(), function (err, id) { | |
if (err) return callback(err); | |
bookmark.id = id; | |
onIdReady(); | |
}); | |
} else { // ID already defined: it's an update | |
this.fetchOne(bookmark.id, function (err, old) { | |
if (err) return callback(err); | |
for (var attr in bookmark) { | |
old[attr] = bookmark[attr]; | |
} | |
bookmark = old; | |
onIdReady(); | |
}); | |
} | |
}, | |
/** | |
* Retrieve a bookmark | |
* callback is called with (err, bookmark) | |
* if no bookmark is found, an error is raised with type=ENOTFOUND | |
*/ | |
"fetchOne": function fetchOne (id, callback) { | |
client.get(_key_(id), function (err, value) { | |
if (!err && !value) err = {"message": "Bookmark not found", "type":"ENOTFOUND"}; | |
if (err) return callback(err); | |
var bookmark = null; | |
try { | |
bookmark = JSON.parse(value); | |
} catch (e) { | |
return callback(e); | |
} | |
return callback(undefined, bookmark); | |
}); | |
}, | |
/** | |
* Retrieve all IDs | |
* callback is called with (err, bookmarks) | |
*/ | |
"fetchAll": function fetchAll (callback) { | |
client.keys(_key_('*'), function (err, keys) { | |
if (err) return callback(err); | |
callback(undefined, keys.map(function (key) { | |
return parseInt(key.substring(namespace.length+1)); | |
})); | |
}); | |
}, | |
/** | |
* Delete a bookmark | |
* callback is called with (err, deleted) | |
*/ | |
"deleteOne": function deleteOne (id, callback) { | |
client.del(_key_(id), function (err, deleted) { | |
if (!err && deleted == 0) err = {"message": "Bookmark not found", "type":"ENOTFOUND"}; | |
callback(err, deleted > 0); | |
}); | |
}, | |
/** | |
* Flush the whole bookmarks database | |
* Note that it doesn't call "flushAll", so only "bookmarks" entries will be removed | |
* callback is called with (err, deleted) | |
*/ | |
"deleteAll": function deleteAll (callback) { | |
var self = this; | |
client.keys(_key_('*'), function (err, keys) { | |
if (err) return callback(err); | |
var deleteSequence = function deleteSequence (err, deleted) { | |
if (err) return callback(err); | |
client.del(_seq_(), function (err, seq_deleted) { | |
callback(err, deleted > 0 || seq_deleted > 0); | |
}); | |
} | |
if (keys.length) { | |
client.del(keys, deleteSequence); | |
} else { | |
deleteSequence(undefined, 0); | |
} | |
}); | |
} | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Module dependencies | |
*/ | |
var contracts = require('contracts'); | |
require('./response'); // Monkey patch | |
/** | |
* Options: | |
* - stack: show stack in error message ? | |
* - log: logging function | |
*/ | |
exports.errorHandler = function (options) { | |
var log = options.log || console.error | |
, stack = options.stack || false | |
return function (err, req, res, next) { | |
log(err.message); | |
if (err.stack) log(err.stack); | |
var content = err.message; | |
if (stack && err.stack) content += '\n' + err.stack; | |
var code = err.code || (err.type == 'ENOTFOUND' ? 404 : 500); | |
res.respond(content, code); | |
} | |
} | |
/** | |
* Checks Accept and Content-Type headers | |
*/ | |
exports.checkRequestHeaders = function (req, res, next) { | |
if (!req.accepts('application/json')) | |
return res.respond('You must accept content-type application/json', 406); | |
if ((req.method == 'PUT' || req.method == 'POST') && req.header('content-type') != 'application/json') | |
return res.respond('You must declare your content-type as application/json', 406); | |
return next(); | |
} | |
/** | |
* Validates bookmark | |
*/ | |
exports.checkRequestData = function (req, res, next) { | |
if (req.method == 'POST' || req.method == 'PUT') { | |
// Body expected for those methods | |
if (!req.body) return res.respond('Data expected', 400); | |
var required = req.method == 'POST'; // PUT = partial objects allowed | |
// Validate JSON schema against our object | |
var report = contracts.validate(req.body, { | |
"type": "object", | |
"additionalProperties": false, | |
"properties": { | |
"url": { "type": "string", "required": required, "format": "url" }, | |
"title": { "type": "string", "required": required }, | |
"tags": { "type": "array", "items": { "type": "string" }, "required": false } | |
} | |
}); | |
// Respond with 400 and detailed errors if applicable | |
if (report.errors.length > 0) { | |
return res.respond('Invalid data: ' + report.errors.map(function (error) { | |
var message = error.message; | |
if (~error.uri.indexOf('/')) { | |
message += ' (' + error.uri.substring(error.uri.indexOf('/')+1) + ')'; | |
} | |
return message; | |
}).join(', ') + '.', 400); | |
} | |
} | |
next(); | |
} | |
/** | |
* Catch and transform bodyParser SyntaxError | |
*/ | |
exports.handleBodyParserError = function (err, req, res, next) { | |
if (err instanceof SyntaxError) res.respond(err, 400); | |
else next(err); | |
} | |
/** | |
* Work on parameter "id", depending on method | |
*/ | |
exports.checkIdParameter = function (req, res, next, id) { | |
if (isNaN(parseInt(id))) { | |
return next({"message": "ID must be a valid integer", "code": 400}); | |
} | |
// Update | |
if (req.method == 'PUT') { | |
if ('undefined' == typeof req.body.id) { | |
req.body.id = req.param('id'); // Undefined, use URL | |
} else if (req.body.id != req.param('id')) { | |
return next({"message": "Invalid bookmark ID", "code": 400}); // Defined, and inconsistent with URL | |
} | |
} | |
// Create | |
if (req.method == 'POST') { | |
if ('undefined' != typeof req.body.id) { | |
return next({"message": "Bookmark ID must not be defined", "code": 400}); | |
} | |
} | |
// Everything went OK | |
next(); | |
} | |
/** | |
* Middleware defining an action on DB | |
* @param action The action ("save", "deleteOne", "fetchAll", etc...) | |
* @param filter An optional filter to be applied on DB result | |
* @return Connect middleware | |
*/ | |
exports.dbAction = function (db, action, filter) { | |
// Default filter = identity | |
filter = filter || function (v) { return v; }; | |
return function (req, res, next) { | |
var params = []; | |
// Parameters depend of DB action | |
switch (action) { | |
case 'save': params.push(req.body); break; | |
case 'fetchOne': | |
case 'deleteOne': params.push(req.param('id')); break; | |
} | |
// Last parameter is the standard response | |
params.push(function (err, result) { | |
err ? next(err) : res.respond(filter(result)); | |
}); | |
// Execute DB action | |
db[action].apply(db, params); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "application-name" | |
, "version": "0.0.1" | |
, "private": true | |
, "dependencies": { | |
"express": "2.4.4" | |
, "ejs": ">= 0.0.1" | |
, "redis": "*" | |
, "contracts": "*" | |
} | |
, "devDependencies": { | |
"api-easy": "*" | |
, "vows": "*" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var http = require('http'); | |
/** | |
* Monkey-patch adding "res.respond(...)" | |
* Usages: | |
* - res.respond(content as string or object) → 200 OK, with JSON encoded content | |
* - res.respond(status as number) → given status, with undefined content | |
* - res.respond(content, status) → ok, you got it :) | |
*/ | |
http.ServerResponse.prototype.respond = function (content, status) { | |
if ('undefined' == typeof status) { // only one parameter found | |
if ('number' == typeof content || !isNaN(parseInt(content))) { // usage "respond(status)" | |
status = parseInt(content); | |
content = undefined; | |
} else { // usage "respond(content)" | |
status = 200; | |
} | |
} | |
if (status != 200) { // error | |
content = { | |
"code": status, | |
"status": http.STATUS_CODES[status], | |
"message": content && content.toString() || null | |
}; | |
} | |
if ('object' != typeof content) { // wrap content if necessary | |
content = {"result":content}; | |
} | |
// respond with JSON data | |
this.send(JSON.stringify(content)+"\n", status); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment