I've seen examples of Restify where all the endpoints are located on the root: /users, /data, etc. I know it's possible to implement nesting like so:
server.get('/users/:user/data/:id', returnData);
and the req.params variable will have all of the request parameters. Example:
{ user: '45', id: '80' }
This seems to work fine if my application has few endpoints, but what if I have a deep and branched data structure that I want to expose through a REST API? Something like:
{
stuff: {
points: {
colors: {
shinyThings: {},
dullThings: {}
}
},
ships: {
enterprises: {},
starDestroyers: {}
}
},
things: {},
}
Having to write the paths to all of these endpoints by hand just doesn't seem right. I end up with lots of path definitions and stuff like this:
server.put('/stuff/:stuff/points/:points/colors/:colors/shinyThings/:shinyThings', returnShinyThing);
Is there an easier way to do this with Restify?
I've come up with a way to do it, although I'm sure there are better alternatives:
1) Create modules to handle certain actions on endpoints. These modules will be required into a central router module. Example stuff.js
:
exports.list = function(req, res, next) {
// Code to handle a GET request to /stuff
};
exports.create = function(req, res, next) {
// Code to handle a POST request to /stuff
};
exports.show = function(req, res, next) {
// Code to handle a GET request to /stuff/:id
};
exports.update = function(req, res, next) {
// Code to handle a PUT request to /stuff/:id
};
exports.destroy = function(req, res, next) {
// Code to handle a DELETE request to /stuff/:id
};
2) In the router module define a mapping of actions -> http verbs:
var actions = {
list: 'get',
create: 'post',
show: 'get',
update: 'put',
destroy: 'del'
}
3) Create an object representing the data structure like this:
var schema = {
stuff: {
_actions: require('./stuff'),
points: {
_actions: require('./points'),
colors: {
_actions: require('./colors'),
shinyThings: {_actions: require('./shinyThings')},
dullThings: {_actions: require('./dullThings')}
}
},
ships: {
_actions: require('./ships'),
enterprises: {_actions: require('./enterprises')},
starDestroyers: {_actions: require('./starDestroyers')}
}
},
things: {_actions: require('./things')},
}
4) During the router initialization the application passes it a Restify server object to attach the routes to. During initialization a recursive function walks the schema object and when a _actions
key is found it calls a second function that attaches the route handlers at the given path to the given server object:
(function addPathHandlers(object, path) {
for (var key in object) {
if (key === '_actions') addActions(object, path);
else if (typeof object[key] === 'object') {
var single = en.singularize(path.split('/').pop());
if (path.charAt(path.length - 1) !== '/') {
path += ['/:', single, '_id/'].join('');
}
addPathHandlers(object[key], path + key);
}
}
})(schema, '/');
function addActions(object, path) {
// Actions that require a specific resource id
var individualActions = ['show', 'update', 'destroy'];
for (var action in object._actions) {
var verb = actions[action];
if (verb) {
var reqPath = path;
if (individualActions.indexOf(action) !== -1) reqPath += '/:id';
server[verb](reqPath, object._actions[action]);
}
}
}
Notes: This makes use of the lingo module (i.e. the en.singularize() function). It's also a bit simplified since I removed non-critical parts of the functions, but it should be fully functional as it is.
The inspiration for this came after looking at the way express-resource does it, although it's not as refined and simple to use.