The Roads.js isomorphic web framework

Roads is a web framework built for use with async functions. It’s similar to Koa.js, but can be used both in the browser and on the server.

Why should I use Roads?

  1. Roads can be attached to any node HTTP server, including Koa.js, Express.js, and the built in node HTTP server.
  2. Roads is isomorphic, meaning you can generate html on the server or in the browser with the same code.
  3. Roads lets you work without callbacks. It’s built on top of promises and async functions.
  4. Roads can be run without ever attaching it to an HTTP server. This is great for writing tests, working with web sockets, or writing API first websites.

Build Status

Build status

Index

Getting Started

Building a project with roads is very straightforward.

  1. Create your Road object
     const roads = require('roads');
     var road = new Road();
    
  2. Add code to the road
     const roads = require('roads');
     var road = new Road();
     road.use(function (method, path, body, headers) {
         console.log('A ' + method + ' request was made to ' + path);
     });
    
  3. Run your code.

Now that you can use your road, continue reading the docs below for more information on routers, error handling, PJAX support and more!

Roads.Road

A Road is a container that holds an array of functions. It exposes a request method which allows you to execute the functions and provide consistent parameters.

new Road()

Create a Road.

Creates your Road object.

const roads = require('roads');
var road = new roads.Road();

Road.use(Function fn)

Add a custom function that will be executed with every request.

The use function can be called one or more times. Each time it is called, the function provided via the fn parameter will be added to a queue that is executed when you call the request method. The execution order will match the order the functions were added to the road.

name type required description
fn Function(string method, string url,object body,object headers,function next) yes Will be called any time a request is made on the object.

Each function can choose whether or not it wants to progress to the following function by calling or ignoring the next parameter. The next parameter is defined below as part of the use callback, along with all other parameters that will be passed to your function when the route is executed.

use Callback

function (string method,string url, string body, Object headers, Function next)

name type description
method string The HTTP method that was provided to the request
url string The URL that was provided to the request. To have separate code for each url, see the SimpleRouter middleware
body string The body that was provided to the request. To parse this see the parseBody middleware.
headers object The headers that were provided to the request
next function The next step of the handler chain. If there are no more custom handlers assigned, next will resolve to the resource method that the router located. This method will always return a promise.

If the callback does not return a response object, roads.request will wrap whatever the callback returns in a response object with the default status code of 200.

// Simple example that sends a JSON response
road.use(function (method, url, body, headers, next) {
    return JSON.stringify({
        method: method,
        url: url,
        body: body,
        headers: headers,
        next: next
    });
});

// Simple async example that sends a JSON response
road.use(async function (method, url, body, headers, next) {
    let db_record = await myPromiseReturningDBCall();

    return JSON.stringify(db_record);
})


// Example of a request handler that kills trailing slashes (This is code is provided for you at roads.middleware.skillSlash!)
// The logic will happen before any other middleware because it's before you call next
road.use(function (method, url, body, headers, next) {
	// kill trailing slash as long as we aren't at the root level
    if (url.path != '/' && url.path[url.path.length - 1] === '/') {
        return new roads.Response(null, 302, {
            location: url.path.substring(0, url.path.length - 1)
        });
    }

    return next();
});

// Example of a request handler that catches errors and returns Response objects
// The logic will happen after any other middleware because it's in response to the promise result of next();
road.use(function (method, url, body, headers, next) {
    // execute the actual resource method, and return the response
    return next()
        // Catch any errors that are thrown by the resources
        .catch ((err) => {
            // Wrap the errors in response objects. If they are [HttpErrors](#roadshttperror) we adjust the status code
            switch (err.code) {
                case 404:
                    return new roads.Response(notFoundRepresentation(err), 404);
                case 405:
                    return new roads.Response(notAllowedRepresentation(err), 405);
                case 500:
                default:
                    return new roads.Response(unknownRepresentation(err), 500);
            }
        });
});

Road.request(string method, string url, dynamic body, Object headers)

Locate and execute the resource method associated with the request parameters.

This function will execute all of the functions assigned via use in the order they were assigned and return a thenable (Promises/A compatible promise). The thenable will always resolve to a Response object.

If the system encounters an unexpected error, it will try to throw an HttpError with a 500 status code. There may be circumstances where we were unable to wrap the exception with an HttpError, so be sure to check whether or not your exceptions are instance of HttpError.

let HttpError = require('roads').HttpError;

var promise = road.request('GET', '/users/dashron');

promise.then((response) => {        
    console.log(response);
});

promise.catch((error) => {
    if (error instanceof HttpError) {
        // handle http errors
    } else {
        // handle all other errors
    }
});
Context

Each middleware function has access to a request context through this. Each this will be unique to the request and will be reused for each request handler (assigned via use). The context is pre-loaded with two variables, listed below.

name type description
request function An alias for Road.request. If you need to make a roads request from within middleware, it is recommended to use this instead of Road.request.
Response Response The Response constructor. Every request should return a Response, so this is provided to simplify the process. If you are constructing a Response from within middleware it is recommended that you use this instead of Roads.Response.

Note: Do not use arrow functions if you want to interact with this. JavaScript is unable to transmit the custom this object into arrow functions.

var road = new Road();

road.use(function (method, url, body, headers, next) {
    this.extraInfo = 'hello!';
    return next();
});

road.use(function (method, url, body, headers) {
    // true because the middleware persists to this context
    console.log(this.extraInfo === 'hello!');


    // Easy access to the response object
    return new this.Response('hello world', 200);
});

road.request('GET', '/me').then((response) => {
    console.log(response);
});

Middleware is encouraged to add variables to this context to simplify development. Make sure to namespace your variables to ensure there are no conflicts with other librares.

eg:

var road = new Road();
road.use(function (method, url, body, headers) {
    this.context.my_project.require_authentication = true;
});

Roads.Response

The response object contains all of the information you want to send to the client. This includes the body, status code and all applicable headers.

new Response(mixed body, number status, Object headers)

Constructor

name type description
body mixed The body of the response.
status number The HTTP Status code.
headers object Key value pairs of http headers.

Create a response object.

new Response({"uri": "..."}, 200, {"last-modified":"2014-04-27 00:00:00"});

Body

The raw JavaScript object returned by the request

console.log(response.body);

Status

The HTTP status returned by the request

console.log(response.status);

Headers

A JavaScript object of all response headers

console.log(response.headers);

Roads.HttpError

new HttpError(string message, number code)

A helper error, that contains information relevant to common HTTP errors

name type description
message string A message describing the HTTP error
code number An official http status code
throw new Roads.HttpError('Page not found', 404);

Constants

These constants make it easier to keep track of some common error status codes. For more information on what they mean, check out (httpstatus.es)[http://httpstatus.es].

HttpError.invalid_request = 400;
HttpError.unauthorized = 401;
HttpError.forbidden = 403;
HttpError.not_found = 404;
HttpError.method_not_allowed = 405;
HttpError.not_acceptable = 406;
HttpError.conflict = 409;
HttpError.gone = 410;
HttpError.unprocessable_entity = 422;
HttpError.too_many_requests = 429;
HttpError.internal_server_error = 500;

Roads.middleware

cors(Array|string allow_origins, array allow_headers)

Middleware to Apply proper cors headers

Sets up the proper preflight, and standard repsonse headers so that browsers can make proper CORS requests.

name type description  
allow_origins Array string Either * to allow all origins, or an explicit list of valid origins.
allow_headers Array (optional) A white list of headers that the client is allowed to send in their requests  
road.use(roads.middleware.cors(['http://localhost:8080'], ['authorization']));

killSlash()

Middleware to kill the trailing slash on http requests

If used, any url that ends with a trailing slash will return a response object redirecting the client to the same url without the trailing slash (302 redirect with Location: [url_without_slash])

road.use(roads.middleware.killSlash);

parseBody

Middleware to parse the request body

This middleware looks at the Content-Type header, and uses that information to attempt to parse the incoming request body string. The body will be applied to the context field body

road.use(roads.middleware.parseBody);

road.use(function (method, url, body, headers) {
    console.log(body); // The string '{"name":"dashron"}'
    console.log(this.body); // The parsed object { name : "dashron" }
});

road.request('POST', '/users', '{"name":"dashron"}', {"content-type": "application/json"});

SimpleRouter(Road road)

This is a simple router middleware for roads. To use it, you have to take a couple of steps.

  1. Create your road
    const roads = require('roads');
    var road = ...; // See steps 1 and 2 in the introduction for road construction
  1. Create your Router
    let router = new roads.middleware.SimpleRouter(road);
  1. Assign routes to the router
    // This is a simple route with no URI variables
    router.addRoute('GET', '/posts', (url, body, headers) => {
        // url, body and headers are all identical to the values sent to functions in roads.use
    });

    // This route supports numeric variables
    router.addRoute('GET', '/posts/#post_id', (url, body, headers) => {
        // url.args.post_id will contain the integer from the URL.
        // e.g. GET /posts/12345 will have url.args.post_id === 12345
    });

    // This route supports any variable
    router.addRoute('GET', '/posts/$post_slug', (url, body, headers) => {
        // url.args.post_slug will contain the value from the URL.
        // e.g. GET /posts/my-post will have url.args.post_slug === 'my-ost'
    });

You can assign functions to url paths, and those paths can have some very basic variable templating. See addRoute for more details.

SimpleRouter.applyMiddleware(Road road)

If you don’t provide a road to the SimpleRouter constructor, the router will not be used. This function allows you to assign the router to the road middleware.

SimpleRouter.addRoute(string method, string path, function fn)

Templating is simple. Each URI is considered to be a series of “path parts” separated by slashes.

Any variables will be added to the route’s request url object under the “args” object.

e.g. /users/#user_id will match /users/12345, not /users/abcde. If a request is made to /users/12345 the route’s requestUrl object will contain { args: {user_id: 12345}} along with all other url object values

    // This is a simple route with no URI variables
    router.addRoute('GET', '/posts', (url, body, headers) => {
        // url, body and headers are all identical to the values sent to functions in roads.use
    });

    // This route supports numeric variables
    router.addRoute('GET', '/posts/#post_id', (url, body, headers) => {
        // url.args.post_id will contain the integer from the URL.
        // e.g. GET /posts/12345 will have url.args.post_id === 12345
    });

    // This route supports any variable
    router.addRoute('GET', '/posts/$post_slug', (url, body, headers) => {
        // url.args.post_slug will contain the value from the URL.
        // e.g. GET /posts/my-post will have url.args.post_slug === 'my-ost'
    });

SimpleRouter.addRouteFile(string full_path)

Add an entire file worth of routes.

The file should be a node module that exposes an object. Each key should be an HTTP path, each value should be an object. In that object, each key should be an HTTP method, and the value should be your route function.

Example File:

{
    '/posts/#post_id': {
        'GET': (url, body, headers) => {

        }
    }
}

Roads.build(string input_file, string output_file, object options)

Browserify function to convert your script to run in the browser

name type description
input_file string The source file that will be converted to use in the browser
output_file string The output file that will be accessible by your browser
options object A set of options that can influence the build process. See all fields below
options.babelify object An object containing parameters to pass to the babelify transform
options.envify object An object to pass to envify. This allows you to change values between your server and client scripts.
options.exclude array An array of files that should not be included in the build process.
options.external array An array of dependencies that should be included from exernal resources instead of built into the project
options.use_sourcemaps boolean Whether or not the build process should include source maps.
require('roads')
    .build(__dirname + '/static/client.js', __dirname + '/static/client.brws.js', {
        use_sourcemaps: true,
        external: {
            roads: {
                output_file: __dirname + '/static/roads.brws.js',
            }, 
            react: {
                output_file: __dirname + '/static/react.brws.js',
            }
        },
        babelify: {presets: ['react']}
    });

Roads.PJAX(Object road, DomElement container_element, Object window)

A helper object to easily enable PJAX on your website using roads

PJAX stands for pushState + AJAX. PJAX a technique for speeding up webpages by replacing certain links on the page with AJAX calls. To enable PJAX, you must create a PJAX object and call the register method. Until you call register, PJAX links will not be handled properly.

PJAX.register()

On page load, you will need to construct and register your PJAX handler. When you click certain links in the container element, PJAX will intercept the action and instead use roads to generate the new page content. Once the new html is ready, it will replace the innerHTML of the container_element.

This allows for clean, quick page refreshes via JavaScript, with a safe, JavaScript free fallback (the links will still work as normal without JavaScript!).

var road = ...; // Incomplete. See the getting started section for more information about creating a road
var pjax = new require('roads').PJAX(road, document.getElementById('container'), window);
pjax.register();

If you would like a link to run via PJAX instead of a new page load, simply add the following data attribute

data-roads-pjax="link"

e.g.

<a href="/home" data-roads-pjax="link">Home</a>

PJAX Page titles

To handle page titles you will need to add matching middleware to your client and server roads. Roads already includes a simple form of this via the setTitle middleware, and the PJAX function addTitleMiddleware.

Your server should include the following:

var roads = require('roads');
var road = ...; // Incomplete. See the getting started section for more information about creating a road
road.use(roads.middleware.setTitle);

var roads = require('roads');
var road = ...; // Incomplete. See the getting started section for more information about creating a road

var pjax = new roads.PJAX(road);
pjax.addTitleMiddleware();
pjax.register(window, document.getElementById('container'));

Isomorphic PJAX tips

There’s a very easy pattern to follow to ensure sharing client and server code works successfully via PJAX. You can see this pattern in more detail in the examples folder

  1. Keep your backend only, and the mixed frontend-or-backend routes in separate files
  2. Have a “server” script that gets your backend server running, applying both backend and mixed routes
  3. Have a “build” script that compiles your frontend server, applying only the mixed routes.
  4. Keep your page layout (headers, footers, body, meta tags, etc.) in the “backend only” section.
  5. Keep your DB calls in the “backend only” section
  6. Make sure the mixed frontend-or-backend routes only ever make HTTP requests, or render HTML

TODO: