At the core of any web application is a mapping between urls and application logic; a mapping between what is in the browser's address bar and what should be displayed on the screen. Rails has routes.rb, Django has URLconf, Backbone.js has
controllersBackbone.Router, and Sammy.js has Sammy.js.
We also aren't talking about arbitrary URLs here. Modern well-designed applications use clean, semantic, and probably RESTful urls. These are urls which are human-readable, have a natural hierarchy to them, and intuitively reflect the underlying data to which they relate.
So, what's the problem?
Problem: Routers Are Designed for Stateless Servers
Historically, url routing systems are designed for web servers which maintain little to no state about the client which is making the request1. A typical request/response lifecycle would look something like the following:
- A url is requested.
- The server loads all resources corresponding to the url.
- A response is rendered and sent back to the client.
- All server-side resources are released.
By not maintaining state, the server is able to drastically reduce its memory-footprint. With this scheme in mind, it makes sense for routing systems to be nothing more than a mapping from url patterns to actions, an example of which is represented in the diagram below:
Here there are four different actions:
comments, with corresponding url patterns. When a url for one of these actions is requested, the application must load the resource or collection, ensure that the current user has the required permissions to view the page, and render one or more templates.
State is Ridiculously Cheap on the Client
Solution: Stateful Routes
A solution for this is to incorporate a more stateful approach to routing. Common state between actions can be extracted into additional states. Consider the following re-factorization:
The above diagram still captures that same url structure as the first diagram, but an intermediate state has been created which encapsulates some common logic. The
comments actions all share a parent
item state. The
item state has some common concerns inside of it that are shared by all its children. Namely, the loading of the resource corresponding to the id, checking read permissions, and loading a common layout.
The idea here is that each leaf state corresponds to a route and as a route is loaded, each state along its path from the root is entered. Edges between states correspond to optional url fragments. Thus, when the url is changed to
items/1, both the
item and the
show states are loaded. Similary, when the url is changed to
comments states are loaded.
So far you might be relatively unimpressed. Sure this is a nice declarative approach to moving some cross-cutting concerns into a better location, but it could be argued that something like controller inheritance could accomplish the same thing. Bear with me.
Stateful Transitions Are Awesome
This becomes really cool when you take into account transitioning from one state to another. For instance, consider the transition on the client from the url
items/1/edit. This corresponds to a transition from the
show state to the
edit state. This is captured in the diagram below:
Since both the
edit states share a common parent state, there is no need for it to be re-entered. All of the concerns inside the
item state have already been addressed and it is trivial for the client to know to just change the view.
Transitions between states can also be dicated by more than url fragments. States can be defined by whether a user is logged in, if a post has been published, if the user is an admin, etc. This makes it extremely easy to turn off or provide alternate behavior for entire groups of routes based on application state. Just for fun, here is a diagram for such a case:
Here, there are different routing trees depending on whether the user is logged in or not.
Ember.js RouteManager Plug
This is the part of the blog post where I shamelessly plug a project, Ember RouteManager, which is routing system for Ember.js built with the design I have given in this post. I'm not going to include any code samples here, but I recommend you check it out!
1. There do exist plenty of stateful server-side web frameworks (continuations are cool), but they could also benefit from stateful routing.