Between Deploys


Engineering Blog

Aviator: Centralized Client-Side Routing

Published on 04 Nov 2013 by Bart Flaherty

URLs are core to how the web works. They should be first class citizens in your web applications.

At Swipely, we have a large client-side application. YUI has helped us keep our codebase nice and modular. But we didn’t love the way our route declarations were dispersed amongst a number of controller-like router objects, each dispatching for a subset of the paths available to the user.

We considered refactoring our application's routing as a YUI-specific pattern, but ultimately decided on a new strategy that would free us from future dependencies. The result was Aviator, a front-end router that plugs in easily to any JavaScript application.

Centralize your routes

We believe routes should be defined in one place, and declared separately from the logic that handles the consequences of visiting a particular route. Some benefits of this approach:

  • application URL hierarchy is easy to grok at a glance
  • controllers/route targets are not bloated with route setup and logic
  • you can be confident about multiple actions occurring in a specific order for any route

Abstract what's browser-specific

The different ways browsers handle URLs and history should not be top of mind when you are writing application code.

So with Aviator, you write your hrefs in your links normally, and Aviator will figure out whether or not it needs to use hash-based routing. Same goes for navigating to a route in code as the result of some action.

Separate Concerns

Aviator doesn't care how you render your views, how you load your data, or what libraries and frameworks you do it with. Its single responsibility is to call the right method on the right object.

We call these objects route targets, and it's entirely up to you what they look like. At Swipely, they serve as controllers; they instantiate views and models and load the data.

It’s fine if one route target receives all the requests, and that might make sense for a small application. But Aviator really shines by making it easy to delegate the route handling to different targets, keeping each one small and focused.

Set up Aviator

Set up is declarative. Call Aviator.setRoutes with a nested object. The object lays out all routes and corresponding actions to take for those routes.

Let's say you have an application where users upload documents and keep track of their document revisions.

  '/docs': {
    target: documentRouteTarget,
    '/*':   'renderLayout',
    '/':    'list',

    '/:documentID': {
      target: documentRouteTarget,
      '/':    'show',

      '/revisions': {
        target:         revisionsRouteTarget,
        '/':            'list',
        '/:revisionID': 'show'

Given this setup, Aviator knows that when a user goes to /docs/2013_potluck_guest_list/revisions/20130722140801?readonly=true, the steps to take are:

  1. call documentRouteTarget#renderLayout
  2. call revisionsRouteTarget#show

This really lends itself to specializing the responsibilities of your route targets. You could imagine one route target that tracks metrics, and gets called on every request, or another that handles a sidebar, but only on some requests. It's easy to be explicit about these things with the nested object setup.

Within the renderLayout and show functions, you get a request object as a parameter. So you have access to the documentID, the revisionID, and the readonly flag through request.params. You can also always get the URI, the query string, and the route that mapped to this function.

Navigate the World

Aviator exposes a small API. Besides setRoutes for configuration, it handles functionality that really should be taken care of by a router rather than by application code.

To name a few, the Aviator object abstracts navigating to a route, updating the URL silently, serializing query params and accessing existing params.

Try it out

Aviator is open source! We’d love to hear how it plugs in to your application. Check it out on github.

Bart Flaherty