Doing is the best form of learning September 27, 2014
REST. It's one of those things that most of us developers typically deal with on the client side of things, or if we deal with them on the server side, we're using an already written API which "REST-ifies" our data. It was somewhat of a mystery to me, and a challenge, to know how to implement an abstract RESTful interface. I had written the webserver I use in node.js which supports all kinds of things, but hadn't yet added REST support. Until last night!
It wasn't too much of an undertaking, I guess I implemented the webserver in such a way that could be easily extended with REST. That's good, go "past Jason".
Basically the server gets the request, attempts to rewrite the URL given the site's configuration of URL Rewrites (which I wrote), then it gets the content (server side processing), and writes the content on the response stream, along with doing cookie and header processing, g-zipping, cache headers, etc. Nearly everything a mature web server should do. It's been around since March 2011 and I always go back to making it better.
I started adding REST to it last night around 10pm, and by 12:30 I was writing code against it using AngularJS. The REST format is inspired by AngularJS, in that you can provide parameters expected in the format :id (colon - name).
I always start with how I want to write code. This, I find, is a very important aspect of how I architect things. I don't want to write a whole bunch of code that I'll have to write each time I want to use the new feature I am adding. So I START with the code that I'll be writing to use the new feature. If this hasn't yet sunk in as to how important I think this is, let me add this sentence... There. It's very important to me and how I architect.
I know the architecture of my web server, and know that you can't just reference things inside of it that haven't been given a public interface. It really has no public interface. The web applications implement the interface to work within the web server. I am looking at a way to decouple them but for now, this is how it is. The "params" array was added in that way because of this.
this.get = {
path: "/:id",
handler: function(db, id, qs, callback){
engine.getObject(db, id, callback);
},
method: "GET",
params: [
function(context){ return context.db; },
"id"
]
}
So in my site code, I'm registering a "get" method that takes the id of an object, looks in the database, and returns it. A URL for the call may look like this: /resource/objects/53f810db8a8cda084e000001
Here's some node.js code that comes from my "resource" module. Within the resource module, you register resources and later check the URL and http method against the current resources, to see if this is a resource / REST call.
this.registerResource = function(domain, name, resource){
var r = /:([a-zA-Z0-9]+)/g;
var m = null;
for (var i in resource){
var res = resource[i];
var regexString = globalPart + name + res.path;
while ((m = r.exec(res.path))!=null){
regexString = regexString.replace(":" + m[1], "(.*?)");
}
var local = { domain: domain, name: name, method: res.method, regex: new RegExp(regexString), handler: res.handler, path: res.path, params: res.params };
resources.push(local);
}
}
For the resource, for which the above "get" code is just one method on a resource, find all the methods, replace the URL with a regex. Instead of the "path" which would be "objects/:id", it creates "/resource/objects/(.*?)", stores the original path, the method to handle it, the http request method (GET, POST, PUT, DELETE currently supported), and the params array.
When a request is made, find the resource with the following code, if no resource is found, or no resources on the current domain that match the HTTP Method, it's a standard request.
this.getResource = function(domain, url, method){
var domainResources = resources.filter(function(r){ return r.domain == domain && r.method == method });
if (domainResources.length == 0) return null;
var resource = null;
domainResources.forEach(function(res){
if (url == globalPart + res.name + res.path) resource = res; // prefer exact matches first
else if (resource == null && res.regex.test(url)){
resource = res;
}
})
return resource;
}
The next methods call the resource handler. For GET / DELETE calls, the requestData is just the querystring, for PUT / POST calls, this will be the form data as parsed by the POST parser in node.
this.extractParamMap = function(url, resourcePath){
var m = null, map = {};
var urlParts = url.split("/");
var resParts = resourcePath.split("/");
for (var i = 0; i < resParts.length; i++){
if (resParts[i].indexOf(":") == 0){
map[resParts[i].substring(1)] = urlParts[i];
}
}
return map;
}
this.handleResource = function(resource, url, context, requestData, callback){
var params = [];
var paramMap = this.extractParamMap(url, globalPart + resource.name + resource.path);
for (var i = 0; i < resource.params.length; i++){
if (typeof(resource.params[i]) == "function"){
params.push(resource.params[i](context));
}
else if (typeof(resource.params[i]) == "string"){
params.push(paramMap[resource.params[i]]);
}
}
params.push(requestData);
params.push(callback);
resource.handler.apply(resource, params);
}
The code within the webserver which was modified to process resources looks like this. Determine if it's a resource or standard request. Call accordingly.
if (req.method == "POST" || req.method == "PUT"){
post.parseForm(req, function(formData){
if (loadedResource != null){
resource.handleResource(loadedResource, url, site, formData, function(data){
var content = {};
content.contentType = "application/json";
content.content = JSON.stringify(data);
finishedCallback(content);
});
}
else if (handler != null){
query.form = formData;
handler.handlePost(path, query, site, req, function(content){
if (jsonRequest) content.contentType = "application/json";
finishedCallback(content);
});
}
})
}
else if (req.method == "GET" || req.method == "DELETE"){
if (loadedResource != null){
resource.handleResource(loadedResource, url, site, query.querystring, function(data){
var content = {};
content.contentType = "application/json";
content.content = JSON.stringify(data);
finishedCallback(content);
})
}
else if (handler != null){
handler.handle(path, query, site, null, req, function(content){
if (jsonRequest) content.contentType = "application/json";
finishedCallback(content);
});
}
}
else {
finishedCallback({contentType: "text/html", content: url + " - No handler found", statusCode: 404});
}
In AngularJS, with the $resource module, this is cake.
return $resource("/resource/objects/:id", {},
{
list: { method: "GET", isArray: true },
get: { method: "GET" },
save: { method: "POST" },
update: { method: "PUT" },
remove: { method: "DELETE" }
}
);
That's it! Later on I might find I need other things, but that's all the code that was required for now for handling resource / REST style methods. I will have a site up in a few weeks / months that will use this heavily. Then you can see it in action!