This package has been deprecated

Author message:

this package has been deprecated

furl

0.0.1 • Public • Published

furljs

Furl is a configuration manager for Express 4. It has a number of features that help you build and maintain larger websites:

  • Centralization of URL routing information.
  • Generation of internal URLs from function references.
  • Enforcement of prerequisites of request handlers.

Usage Scenario

Dumpling World is a typical e-commerce website. It has a main page, a catalog page listing products on offer, a product details page, and a user profile page. The application's main script is as follows:

'use strict';
 
var http = require('http');
var furl = require('furl');
 
var shopFront = require('./shopFront');
var productCatalog = require('./productCatalog');
var userProfile = require('./userProfile');
 
var server = http.createServer().listen(8088);
 
var parameters = {
    'view engine': 'jade'
};
 
var routes = {
    '/': 
        shopFront.showWelcomePage,
    '/products':
        productCatalog.showProductList,
    '/products/:id':
        productCatalog.showProduct,
    '/users/:id/personal': {
        GET:  userProfile.showPersonalInformation,    
        POST: userProfile.savePersonalInformation,
    },
};
 
furl.set(parameters)
    .route(routes)
    .install(app);

To keep the codebase manageable, we store request handlers in multiple source files: shopFront.js, productCatalog.js, and userProfile.js. We specify the URL routes in an object. The base URL is handled by showWelcomePage in shopFront.js. /products is handled by showProductList in productCatalog.js, and so forth. By default, the HTTP method is assumed to be GET. When a URL can be accessed through both GET and POST, we specify the separate handlers in a object with the method as keys (case insensitive).

The application's parameters are also kept in an object. Here, we specify Jade as the template engine.

We pass the parameter table to furl.set() and the routing table to furl.route(). Then we call furl.install() to apply the configuration information to the HTTP server. The return value of install() will be an Express instance.

The welcome page in our example is very simple. It basically contains a link to the catalog:

html
  head
    title Dumpling World
  body
    h1 #{message}
    a(href=catelogUrl) Check out our products!

shopFront.js looks like this:

'use strict';
 
[showWelcomePage].forEach(function(f) {
    module.exports[f.name] = f;
});
 
var furl = require('furl');
 
var productCatalog = require('./productCatalog');
 
function showWelcomePage(req, res) {
    res.render('welcome', { 
        message: 'Hello there!',
        catelogUrl: furl.find(productCatalog.showProductList),
    });
}

The first thing of note is how we define the request handler as named function and not an anonymous function assigned to module.exports. The reason we do this is because we'll be using function references to obtain URLs. This will happen fairly frequently. It's more convenient when the functions can be referenced by name in the local scope.

The second thing of note is how the request handler is attached to module.exports prior to any call to require(). This arrangement reduces the chance of mishaps when modules require each other. Suppose productCatalog.js requires shopFront.js. If we had placed the initialization of module.exports after require('./productCatalog'), then productCatalog.js will receive a partially initialized copy of shopFront with no exported functions.

Before the request handler renders the page, it calls furl.find() to get the URL of the product catalog. We could have, of couse, simply hard-coded it as "/products". Letting Furl generate the URL dynamically gives us more flexibility though. In the future, we can freely tinker with our URL scheme without fear of introducing broken links. Because the URLs are generated from the routing table, they'll always be correct. If we make a mistake, Furl will let us know immediately by throwing an exception. It saves us from having to click on a link to detect if it's broken.

The fact that we have to require a module before linking to a page it handles is also good from a coding standpoint, as it more accurately reflects relationships between modules. In the example, shopFront.showWelcomePage() clearly depends on productCatalog.showProductList() from a functional standpoint. The explicit reference to latter in the code of the former allows a code analyser to correctly detect that dependency.

Now, let us look at productCatalog.js:

'use strict';
 
[showProduct, showProductList].forEach(function(f) {
    module.exports[f.name] = f;
});
 
var furl = require('furl');
 
var database = require('./database');
 
function showProductList(req, res) {
    database.fetchProducts(function(products) {
        res.render('productList', {
            products: products,
            productUrl: function(p) {
                return furl.find(showProduct, { id: p.id });
            }
        });
    });
}
 
function showProduct(req, res) {
    var id = parseInt(req.params.id);
    database.fetchProduct(id, function(product) {
        res.render('product', {
            product: product
        });
    });
}

The file contains two request handlers. showProductList() fetches all available products from the database and renders a list. showProduct() fetches a single product and shows its details.

As before, the URL to the product details page is dynamically generated. Instead of passing a string to Jade, showProductList() provides a closure that returns a URL for a given product. The route to showProduct() is /products/:id. Furl knows that it requires id as a parameter. If we fail to provide it (as the second argument to furl.find()), Furl will throw an exception.

Parameter Validation

showProduct() expects the parameter id. The handler will only work properly if it's the target of a route containing :id. This prerequisit is not explicitly stated though. Suppose we decide at some point that we decide to employ more specific parameter names. Our routing table becomes:

var routes = {
    '/': 
        shopFront.showWelcomePage,
    '/products':
        productCatalog.showProductList,
    '/products/:productId':
        productCatalog.showProduct,
    '/users/:userId/personal': {
        GET:  userProfile.showPersonalInformation,    
        POST: userProfile.savePersonalInformation,
    },
};

showProduct() now ceases to work, because req.params.id is no longer set by Express. We won't know this though until we visit the page. This is undesirable. In general, we want our mistakes to be revealed as soon as possible so we can quickly fix them. Furl lets us specify a request handler's requirements by attaching an expectation object to the function:

function showProduct(req, res) {
    var id = parseInt(req.params.id);
    database.fetchProduct(id, function(product) {
        res.render('product', {
            product: product
        });
    });
}
showProduct.expectation = {
    params: ['id']
}

When furl.install() is called, Furl will ensure that the expectations of all request handlers are met. When we changed the route to showProduct() to /product/:productId, it no longer meets the handler's expectation. The following error is the result:

Route '/product/:productId' does not meet the expectation of productCatalog.showProduct 

Unmet conditions:
params: id

Suggestions:
Change :productId in the path to :id

In this case, we actually want the parameter to be called productId. So we adjust the function and its expectation instead. After we've done that, the application will start up correctly. As soon as we reach the product list though, we'll get an exception because furl.find() now needs { productId: p.id } and not { id: p.id }. We have to fix that as well.

Besides availability of parameters, Furl can enforce other types of prerequsites. We'll examine these later.

In the example above, we could have replaced ['id'] with 'id':

showProduct.expectation = {
    params: 'id'
}

Furl will automatically convert scalar values to as single-element arrays.

Parameter Filters

When a URL route contains a parameter like :productId, chances are pretty good that we'll need to load the product from the database. Wouldn't it be great if it happens automatically? Express has just such a feature: app.param(). Through Furl, it's accessible through furl.filter().

We modify the main script of our dumpling shop to this:

'use strict';
 
var express = require('express');
var furl = require('furl');
 
var shopFront = require('./shopFront');
var productCatalog = require('./productCatalog');
var userProfile = require('./userProfile');
var contextSetter = require('./contextSetter');
 
var app = express();
var server = app.listen(8000);
 
var parameters = {
    'view engine': 'jade'
};
 
var filters = {
    'productId': contextSetter.loadProduct
};
 
var routes = {
    '/': 
        shopFront.showWelcomePage,
    '/products':
        productCatalog.showProductList,
    '/products/:productId':
        productCatalog.showProduct,
    '/users/:userId/personal': {
        GET:  userProfile.showPersonalInformation,    
        POST: userProfile.savePersonalInformation,
    },
};
 
furl.set(parameters);
furl.filter(filters);
furl.route(routes);
furl.install(app);

furl.filter() accepts an object in which each property is a reference to a filter function. In the example, we're keeping our function in a separate file (contextSetter.js). It contains the following:

function loadProduct(req, res, next, value, name) {
    database.fetchProduct(parseInt(value), function(product) {
        if (product) {
            req.context.product = product;
            next();
        } else {
            res.status(404);
            res.end();
        }
    });
}
loadProduct.fulfillment = {
    context: ['product']
};

Any route that contains :productId will trigger the handler. It will fetch the product object from the database and stores it in req.context. A fulfillment object is attached to the function. It tells Furl that whenever the handler is invoked, the condition "product in context" is met.

In productCatalog.js, we simplify the request handler and adjust its expectation:

function showProduct(req, res) {
    res.render('product', {
        product: req.context.product
    });
}
showProduct.expectation = {
    context: ['product']
}

Looking at our updated code, we should be happy that our request handler no longer has to deal with the complexity of reading data from the database asynchronously. It would be nice if we can do the same for showProductList() as well. That handler doesn't require a parameter though, so we can't use a parameter filter. Instead, we have to use a path filter:

var filters = {
    'productId': contextSetter.loadProduct,
    '/products': contextSetter.loadProducts
};

When furl.filter() encounters a name containing a "non-word" character (not alphanumeric or underscore; \W in RegExp), it'll treat it as a path. The hander is invoked whenever the URL matches the path. Its code looks as follows:

function loadProducts(req, res, next) {
    database.fetchProducts(function(products) {
        if (products) {
            req.context.products = products;
            next();
        } else {
            res.status(404);
            res.end();
        }
    });
}
loadProducts.fulfillment = {
    context: ['products']
};

Path filters make use of Express's app.all() functionality.

We simplify showProductList() as we had done before with showProduct:

function showProductList(req, res) {
    res.render('productList', {
        products: req.context.products,
        productUrl: function(p) {
            return furl.find(showProduct, { productId: p.id });
        }
    });
}
showProductList.expectation = {
    context: ['products']
}

Now our request handlers only deal with a page's presentational aspect. Separation of concerns--we like it.

Let us imagine for a moment that a new developer has joined our team. He's not too familiar with the codebase. He's tasked with creating a page that shows the product list somewhat differently. He looks at the existing code and copy-and-pastes a chunk of it to start out with:

function showProductListForMobileDevice(req, res) {
    res.render('productListMobile', {
        products: req.context.products,
        productUrl: function(p) {
            return furl.find(showProduct, { productId: p.id });
        }
    });
}
showProductListForMobileDevice.expectation = {
    context: ['products']
}

Our new developer add an entry to the routing table, but neglects to add a filter for the new page. When he starts the application, it'll fail with the following error:

Route '/product/specials' does not meet the expectation of productCatalog.showSpecialProductList 

Unmet conditions:
context: products

Suggestions:
Use contextSetter.loadProducts as a filter or middleware

Parameter Substitution

Parameter substitution is the flip-side of parameter filtering. Whereas a parameter filter takes a productId and produces a product object, a parameter substitution rule take a product object and yields the condition productId = . It lets us pass an array containing the object to furl.find(). Furl will then figure out what parameters to use to properly reference the object. Instead of

productUrl: function(p) {
    return furl.find(showProduct, { productId: p.id });
}

we can use

productUrl: function(p) {
    return furl.find(showProduct, [ p ]);
}

The feature is especially useful when multiple variables are needed to form a URL. Imagine if the URL to our product page contains the product category: /products/:category/:productId. Having to continually type { category: p.category, productId: p.id } would get quite tiresome.

Subsitutions are defined using furl.subsitute(). It accept an object as an argument. Each key should be the class name of an object (i.e. name of the constructor). The value should be a function that takes an object and returns the correct parameters. For our example:

var substitutions = {
    'Product': function(p) { return { 'productId': p.id }; }
};

Readme

Keywords

Package Sidebar

Install

npm i furl

Weekly Downloads

1

Version

0.0.1

License

MIT License

Last publish

Collaborators

  • cleong