Advanced Routing
This chapter will cover many different ways to build HTTP routes using jax.server.Controller.
Function Routing
There is an alternate way to define an HTTP route that hasn't been mentioned yet. Instead of defining a route method (a method of the jax.server.Controller), the route can be an anonymous function:
function Helloworld() {
//...
this.get('/functionroute',function(request,response) {
jax.server.sendHTML(request,response,"this is a function route!");
});
}
This format has some advantages like being quicker to define, but some disadvantages such as not being possible to re-use the route. Because the function is anonymous, it cannot be called from other routes. In large applications it is preferable to use methods for readability.
Views can also be called from a function route as long as a local veraible is defined to refer to the controller using this:
function Helloworld() {
//...
var app = this;
this.get('/functionview',function(request,response) {
app.sendView(request,response,'functionview');
});
}
Function Prerouting
A scenario where function routes are particularly useful is to share logic between two routes or to check a password before continuing:
function Helloworld() {
///...
var app = this;
this.get('/preroute',function(request,response) {
if (request.params.password==="sesame") {
app.accessGranted(request,response);
}
else {
app.accessDenied(request,response);
}
});
}
//...
Helloworld.prototype.accessGranted = function(request, response) {
jax.server.sendHTML(request,response,'Password Accepted.');
};
Helloworld.prototype.accessDenied = function(request, response) {
jax.server.sendError(request,response,401,{message:'Access Denied.'});
};
Without password: http://localhost:8000/preroute
With password: http://localhost:8000/preroute?password=sesame
User Agent Routing
A common task when building web applications is to check the web browser's User-Agent identification to determine different actions to take. Legacy web browsers could be blocked from the website, or certain features can be enabled or disabled. To do this in any NodeJS web application, the HTTP request headers property user-agent must be used:
request.headers['user-agent']
Here is the result for the web browser you are using:
CCBot/2.0
A route handler can be modified to send different responses (views or raw html/text) depending on which client is viewing it:
Helloworld.prototype.browserTest = function(request, response) {
var userAgent = request.headers['user-agent'];
if (/Firefox|Chrome/.test(userAgent)) {
jax.server.sendHTML(request,response,'Congrats, your web browser is sufficient');
}
else if (/Safari/.test(userAgent)) {
if (/iPad/.test(userAgent)) {
jax.server.sendHTML(request,response,'Congrats, your tablet is sufficient');
}
else if (/iPhone|iPod/.test(userAgent)) {
jax.server.sendError(request,response,403,{message:'Sorry, smartphones are too small'});
}
else {
jax.server.sendHTML(request,response,'Congrats, your web browser is sufficient');
}
}
else if (/MSIE/.test(userAgent)) {
jax.server.sendHTML(request,response,403,{message:'Sorry, you need to upgrade your web browser'});
}
else {
jax.server.sendError(request,response,401,{message:'Unknown web browser'});
}
}
Test with different browsers: http://localhost:8000/browsertest
Regular Expressions
Instead of explicitly defining the URL for a route, regular expressions can be used instead. This provides the way to define wildcards, or to reject invalid URLs.
This example will match a URL starting with /numbers/ and followed by 2 numbers. The numbers that follows will be available in a special array request.matches which contains the result of the regular expression operation. Any other URL will be ignored by this route and return a default 404 Not Found error response.
function Helloworld() {
//...
// only match /numbers/00 to /numbers/9999
this.get(/^\/numbers\/(\d{2,4})$/,function(request,response) {
var numbers = request.matches[1];
jax.server.sendHTML(request,response,'You have requested: '+numbers);
});
}
/numbers = error
/numbers/ = error
/numbers/5 = error
/numbers/X5 = error
/numbers/05 = success
/numbers/005 = success
/numbers/0005 = success
/numbers/00005 = error
Any group of characters in your regular expression that is surrounded by ( and ) get added to the request.matches array. This example adds another pattern group to the previous example:
function Helloworld() {
//...
// only match /numbers/(00-9999)/letters/(a-z)...
this.get(/^\/numbers\/(\d{2,4})\/letters\/([a-z]+)$/,function(request,response) {
var numbers = request.matches[1];
var letters = request.matches[2];
jax.server.sendHTML(request,response,'You have requested: '+numbers+' and '+letters);
});
}
Regular expressions can also be used to perform prerouting:
function Helloworld() {
//...
var app = this;
// only match /number/0 to /number/9
this.get(/^\/number\/(\d)$/,function(request,response) {
var number = parseInt(request.matches[1]);
if (number<5) {
app.lowNumber(request,response,number);
}
else {
app.highNumber(request,response,number);
}
});
}
Helloworld.prototype.lowNumber = function(request, response, number) {
jax.server.sendHTML(request,response,'Low number: '+number);
};
Helloworld.prototype.highNumber = function(request, response, number) {
jax.server.sendHTML(request,response,'High number: '+number);
};
http://localhost:8000/number/1
http://localhost:8000/number/6
These examples are meant only as a primer to regular expression based routing. Regular expressions require time to learn, a good guide to learn JavaScript regular expression is available at JavascriptKit.com.
REST Interfaces
REST stands for Representational State Transfer. It a common way to write an abstract interface to a data store, whether it be a database, data files, or in-memory data. XML and XML-RPC were once the most common format for REST interfaces, however, using JSON as the data format is gaining popularity due to JavaScript's ubiquity. It is especially easy to write JSON-REST interfaces in NodeJS due to JavaScript being the native language. JaxServer has added a helper method jax.server.sendJSON to assist in sending JSON responses easily.
REST uses the existing HTTP methods to perform create, read, update, and delete (CRUD) operations:
| HTTP Method | CRUD Action | SQL Operation | Description |
|---|---|---|---|
| POST | CREATE | INSERT | Create a new resource |
| GET | READ | SELECT | Read a resource |
| PUT | UPDATE | UPDATE | Update a resource |
| DELETE | DELETE | DELETE | Delete a resource |
Each of those HTTP actions can be defined using jax.server.Controller's post, get, put, and del methods. Note .delete() cannot be used because delete is a reserved word in the JavaScript language, so del must be used instead.
The following creates a REST interface available at the URL /rest. In lieu of a proper database, a temporary in-memory array is used to store the data:
function Helloworld() {
//...
this.restResources = []; // create in-memory data store
this.get('/rest','restList'); // list all resources
this.post('/rest','restCreate'); // create a resource
this.get(/^\/rest\/(\d+)$/,'restRead'); // read a resource
this.put(/^\/rest\/(\d+)$/,'restUpdate'); // update a resource
this.del(/^\/rest\/(\d+)$/,'restDelete'); // delete a resource
}
//...
Helloworld.prototype.restList = function(request, response) {
jax.server.sendJSON(request,response,200,this.restResources);
};
Helloworld.prototype.restCreate = function(request, response) {
var data = {
id : this.restResources.length
};
this.restResources.push(data);
jax.server.sendJSON(request,response,201,{ // 201 Created
success : true,
message : 'created resource '+data.id,
id : data.id
});
};
Helloworld.prototype.restRead = function(request, response) {
var id = parseInt(request.matches[1]);
if (this.restResources[id]) {
jax.server.sendJSON(request,response,200,this.restResources[id]);
}
else jax.server.sendJSON(request,response,404,{error:'resource not found'});
};
Helloworld.prototype.restUpdate = function(request, response) {
var id = parseInt(request.matches[1]);
if (this.restResources[id]) {
// copy all request parameters to the resource
for (var i in request.params) {
if (i!='id') { // we don't want allow updating the id
this.restResources[id][i] = request.params[i];
}
}
jax.server.sendJSON(request,response,202,{ // 202 Accepted
success : true,
message : 'updated resource '+id,
id : id
});
}
else jax.server.sendJSON(request,response,404,{error:'resource not found'});
};
Helloworld.prototype.restDelete = function(request, response) {
var id = parseInt(request.matches[1]);
if (this.restResources[id]) {
this.restResources[id] = null;
jax.server.sendJSON(request,response,202,{ // 202 Accepted
success : true,
message : 'deleted resource '+id,
id : id
});
}
else jax.server.sendJSON(request,response,404,{error:'resource not found'});
};
Notice how regular expressions are used to identify the data elements in the URL using the format /rest/0. The action that is taken on that resource (the route that is processed) depends on the HTTP method used to query the URL:
POST /rest --> creates a resource, first is 0, second is 1 etc... GET /rest/0 --> reads resource 0 PUT /rest/0 --> updates resource 0 DELETE /rest/0 --> deletes resource 0 GET /rest --> reads all the resources
To test a REST interface the most reliable way is to use the curl unix command to send test requests:
// READ ALL
$ curl -X GET http://localhost:8000/rest
[]
// CREATE resource 0
$ curl -X POST http://localhost:8000/rest
{"success":true,"message":"created resource 0","id":0}
// READ ALL
$ curl -X GET http://localhost:8000/rest
[{"id":0}]
// READ 0
$ curl -X GET http://localhost:8000/rest/0
{"id":0}
// UPDATE 0
$ curl -X PUT http://localhost:8000/rest/0 -d "name=First"
{"success":true,"message":"updated resource 0","id":0}
// READ 0
$ curl -X GET http://localhost:8000/rest/0
{"id":0,"name":"First"}
// READ non-existent returns an error
$ curl -X GET http://localhost:8000/rest/1
{"error":"resource not found"}
// CREATE resource 1
$ curl -X POST http://localhost:8000/rest
{"success":true,"message":"created resource 1","id":1}
// UPDATE 1
$ curl -X PUT http://localhost:8000/rest/1 -d "name=Second"
{"success":true,"message":"updated resource 1","id":1}
// READ 1
$ curl -X GET http://localhost:8000/rest/1
{"id":1,"name":"Second"}
// READ ALL
$ curl -X GET http://localhost:8000/rest
[{"id":0,"name":"First"},{"id":1,"name":"Second"}]
// DELETE 0
$ curl -X DELETE http://localhost:8000/rest/0
{"success":true,"message":"deleted resource 0","id":0}
// READ 0
$ curl -X GET http://localhost:8000/rest/0
{"error":"resource not found"}
// READ ALL
$ curl -X GET http://localhost:8000/rest
[null,{"id":1,"name":"Second"}]
// DELETE 1
$ curl -X DELETE http://localhost:8000/rest/1
{"success":true,"message":"deleted resource 1","id":1}
// READ ALL
$ curl -X GET http://localhost:8000/rest
[null,null]
Header information can be obtained by using curl's -I option:
$ curl -I -X GET http://localhost:8000/rest/99 HTTP/1.1 404 Not Found Content-Type: application/json Content-Length: 31 Date: Fri Aug 12 2011 16:49:39 GMT-0400 (EDT) Last-Modified: Fri Aug 12 2011 16:49:39 GMT-0400 (EDT) Cache-Control: no-cache Connection: keep-alive
Nested Controllers
Controllers can be nested inside one another to encapsulate all the routes on indivual directories.
A nested controller is responsible for all HTTP requests upon a particular path. In this example, the NestedController will be responsible for all requests to /nestedcontroller/*.
- GET --> Helloworld.index()
- GET --> NestedController.index()
- GET --> NestedController.test()
- POST --> NestedController.save()
In a new file named nestedcontroller.js, another controller named NestedController is declared:
function NestedController() {
jax.server.Controller.call(this, arguments, __dirname);
this.get('/', 'index');
this.get('/test', 'test');
this.post('/save', 'save');
}
NestedController.prototype = new jax.server.Controller();
NestedController.prototype.index = function(request, response) {
var html = '<h1>NestedController</h1>'+
'<form action="save" method="post">'+
'<input type="hidden" name="value" value="123">'+
'<input type="submit"></form>';
jax.server.sendHTML(request, response, html);
};
NestedController.prototype.test = function(request, response) {
jax.server.sendHTML(request, response, '<h1>NestedController Test!</h1>');
};
NestedController.prototype.save = function(request, response) {
jax.server.sendText(request, response, 'NestedController save:\n\n'+JSON.stringify(request.params));
};
exports.NestedController = NestedController;
Because nestedcontroller.js is saved in the same directory as helloworld.js they will share the same /views and /web subdirectories.
Now back to the Helloworld controller, the NestedController source code is included in the file using NodeJS's require() function and the route is declared using the jax.server.Controller method controller:
// include controller
var NestedController = require('./nestedcontroller.js').NestedController;
function Helloworld() {
//..
var nc = new NestedController();
this.controller('/nestedcontroller', nc);
}
//...
All HTTP requests to /nestedcontroller will now be re-routed automatically to the instance of NestedController. The URL prefix of /nestedcontroller is hidden from the context of NestedController but is available as a property named urlPrefix which can be used in view templates.
View example: http://localhost:8000/nestedcontroller/
Nested Applications
Because all JaxServer applications are controllers, applications can be nested inside one another in any number of permutations.
As an example, a new application can be created:
jax create nestedapp
Within the Helloworld controller, an instance of nestedapp can be instantiated using jax.app.instantiate, and /nestedapp route can be declared for it by again using the controller method:
function Helloworld() {
//..
var nestedapp = jax.app.instantiate('nestedapp');
this.controller('/nestedapp', nestedapp);
}
Filesystem Aliases
JaxServer will serve all files placed in an application's /web/ directory. But additional directories can be mapped (or "aliased") into the web directory. This allows JaxServer to serve files stored outside of the application directory. Aliasing is performed using the jax.server.Controller alias() method:
this.alias( String WEB_URL, String REAL_PATH );
This example maps ~/jaxserver/app/helloworld/aliasdir as a new path on the website called /alias/. The __dirname variable is a NodeJS convention to obtain the working directory of the current JavaScript file.
function Helloworld() {
//..
this.alias('/alias', __dirname+'/aliasdir');
}
All files placed in ~/jaxserver/app/helloworld/aliasdir will then be served as /alias/FILENAME.
http://localhost:8000/alias/
http://localhost:8000/alias/alias.html