Convention Based Routing In JavaScript Apps

by Burke Holland on May 19, 2014

The modern web is always changing, and this article is more than two years old.

routing-javascript-header

By Burke Holland

Larger apps in the browser are demanding better structure. While there is no shortage of MV* frameworks to choose from when it comes to JavaScript, they all have somewhat vague instructions on how to physically structure your application on disc. We often see examples of smaller applications when learning these new frameworks. These smaller examples are great for learning, but don’t scale. Consider all the gray area when looking at any JavaScript framework.

  • How should you structure your project?
  • What should your API look like?
  • How do you share components?
  • How do you handle communication between components?

Angular has done a fairly good job of being the first framework to take a very opinionated stance on some of these issues, but it still leaves quite a bit to the developer. While it’s nice to have Controllers, Views, Routers, Service, Factories and the like, they don’t really help you with an application structure and pattern that will scale.

The Rise Of MVC

Have you ever wondered why we have been historically referring to any JavaScript framework as an “MVC Framework”? If you have done any Rails, ASP.NET MVC, Spring or other true MVC framework coding, you may have looked at these JavaScript frameworks and thought, “That is NOT MVC.” Angular was the first to call themselves a MV* pattern, which is probably a more accurate name than MVC.

The Model View Controller (MVC) pattern became especially popular among web developers with Rails. The reason people loved it is that it is a very simple abstraction that mostly helps with organization of code and a dynamic response to HTTP requests. While JavaScript frameworks that we use today are not true MVC, we can use some of what we learned from Rails and MVC and apply that to our large JavaScript applications. Namely, convention over configuration.

Most of this is impossible, however, without some sort of module system.

RequireJS And App Structure

Module arguments aside, lets look at this in the context of RequireJS. Usually with RequireJS, I will lay out my applications so that they are structured something like this:

  • main.js
  • app.js
  • views
    • home
      • home.js
      • home.html
    • details
      • details.js
      • details.html
    • layout
      • layout.js
      • layout.html

The main.js file specifies the configuration of RequireJS, such as paths and whatnot. The app.js file actually loads all of the different views, which in turn load their own HTML. Each HTML file has a corresponding .js file which is sort of the “controller” for the view although it technically returns a Kendo UI View object. Naming is hard.

Inside the app.js file, I usually create a router and the manually plug-in the different routes.

define([
  'kendo',
  'views/layout/layout',
  'views/home/home',
  'views/details/details'
], function (kendo, layout, home, details) {

  // the application router
  var router = new kendo.Router({
    init: function () {
      // render the layout first
      layout.render("#applicationHost");
    },
    routeMissing: function (e) {
      console.log('No Route Found', e.url);
    }
  });

  router.route('/', function (e) {
    layout.showIn("#content", home);
  })

  router.route('/details', function (e) {
    layout.showIn("#content", details);
  });

  return router;

});

Since I return the actual Router object from the app module, I can just call app.start() in my main file and the application is off and running. I’ve long been quite pleased with this separation and have created Yeoman generators and ASP.NET MVC Templates that follow this pattern. Then today Vesselin Obreshkov brought up something that I had not thought about yet. I’ll just post his comment here word for word since he says it better than I do.

vesselin-comment

This is something I had not thought of before. Do we really need to define every route? Can’t the framework be smart enough to load the right “controller” based on the current route?

Before we proceed, lets look at how our much more road hardened and mature server framework counterparts handle this issue.

Rails

Rails routing is just flat-out gorgeous. By default, you get 7 RESTful routes for any given resource. According to the Rails documentation:

“When your Rails application receives an incoming request for ‘DELETE /photos/17’ it asks the router to map it to a controller action.

In other words, as long as you have defined a resource, Rails will automatically give that resource the 7 RESTful routes and map them to their corresponding actions based on naming convention. This means that…

resources: photos

…will generate routes for…

rails-routes

source: Ruby on Rails Documentation: http://guides.rubyonrails.org/routing.html#crud-verbs-and-actions

Like I said though, Rails routes are sexy and you can get all sorts of control over them without much work. You can define multiple resources at one time and even namespace them.

Rails also has the concept of Non-Resourceful Routes, which just maps requests to actions based on a very hard wired specification. However, they recommend using Resourceful Routes when possible since once you begin hard coding routes, you are well into “configuration” land which grows difficult to maintain quite quickly.

ASP.NET MVC

The ASP.NET MVC framework has been famously popular with .NET developers and for a great reason: it’s incredibly well done. One of the things that ASP.NET MVC does out of the box that I really like is it’s default configuration for routes. For instance, when you create a new MVC application, you will get a RouteConfig file that is loaded in on application start.

public class RouteConfig {
  public static void RegisterRoutes(RouteCollection routes) {
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
      name: "Default",
      url: "{controller}/{action}/{id}",
      defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
  }
}

This means that when you execute a request for /, MVC automatically looks for a file in the “Controllers” folder called “HomeController”. It treats anything after that as the method it should execute and then the last parameter is an id. WebAPI handles this still a bit differently in the details, but relies on the same routing configuration on app start.

This causes a lot of developers (including myself) to assume that routing “just works” in ASP.NET MVC, because it just does.

Event better is the Attribute Based Routing library on NuGet allows you to specify routes simply by decorating methods.

public class ProductController : Controller {

  ['products/get']
  public void get() {
    // return some products i guess...
  }

}

Now THAT is how you do routing. At least, I like it a lot.

Now that we’ve seen how other frameworks do this, it’s a little bit clearer that what we’re doing on the JavaScript side by declaring every single route is a bit juvenile. It’s rather naive to assume that we can go on statically declaring routes and not hit a ceiling very soon. If we are to embrace the client, we need a way to route based on convention so that our applications can scale.

Convention Based Routing In JavaScript

Fortunately, RequireJS/CommonJS (Browserify) make breaking your JavaScript into multiple files and loading/executing files dynamically quite easy if your JavaScript router can handle it. Before we can make that happen though, we need to change up our naming a bit to establish some sort of convention that we can rely on when trying to load in a controller based on the url.

Whats In A Controller?

The definition of what a “Controller” is or should do got blown out of the water a long time ago. In the context of Kendo UI, what I’m calling a “Controller” is just an object which holds a view model, loads in the HTML for the view, and returns the Kendo UI view object. Angular has a more formal definition for a controller, but it’s just an object too. At the end of the day, everything in JavaScript is “just an object”. You could argue that I could break this out even more by putting the view models into their own folder so they can be shared between controllers.

In any event, I created a folder named “Controllers” and moved all of the corresponding .js files from the various views inside. I could have named them homeController.js, viewController.js and so on, but they are already in a folder called “controllers”. I think that should make it clear what they are. The views all stay in the same place with the exception that I decided to follow ASP.NETs convention and name them all index.html. Now our structure is starting to make a bit more sense.

  • main.js
  • app.js
  • controllers
    • home.js
    • details.js
    • layout.js
  • views
    • home
      • index.html
    • details
      • index.html
    • layout
      • index.html

Routing Based On Convention

The Kendo UI Router has a route method which takes a route (string) and a callback. This is how you normally define routes. It’s possible to use a regular expression instead of a string to match all routes, but I’m not going to do that and I’ll explain why in a bit. I’m going to use the routeMissing method instead. This method is fired whenever there is a route requested that has not been defined on the router.

This method receives an event object which contains the URL. A little bit of string manipulation will split off the parameters (which are in a separate object on the event) and leave just the path. Then, instead of requiring all the controllers in at the top, we can dynamically require one in based on convention alone.

define([
  'kendo',
  'controllers/layout'
], function (kendo, layout) {

  // the application router
  var router = new kendo.Router({
    init: function () {
      // render the layout first
      layout.render("#applicationHost");
    },
    routeMissing: function (e) {
      // assume the view is the same as the route
      var path = e.url.split('?')[0];

      // require it in, accounting for the root (/) and replacing that with '/home'
      require([ 'controllers/' + (path === '/' ? '/home' : path) ], function (view) {
        layout.showIn('#content', view);
      });
    },
    change: function (e) {
      // publish an event whenever the route changes
      $.publish('/router/change', [e]);
    }
  });

  return router;

});

Now anytime we create a new view, we just add the HTML for the view to the views folder and create the controller file which returns the Kendo UI view object. It’s possible to additionally load the HTML based on its convention, but I’m not convinced that’s an entirely necessary abstraction.

This works wonderfully, but it does gloss over a huge issue here, and that is parameters.

Like Rails, the Kendo UI Router supports bound parameters, optional segments, and globbing. In our current setup, the only way we can get parameters in is on the query string. Everything else will get mapped to a controller. In other words, if you want /details/59 to load the controllers/details.js file with a parameter of “59”, you won’t get it. Instead, it will try to load controllers/details/59.js. There are three possible solutions to this issue.

  1. Only use query string parameters
  2. Define routes that need parameters
  3. Modify the convention to account for the ID

Both Rails and MVC factor this into their convention. This is one of the reasons that I LOVE attribute based routing in ASP.NET MVC. In my experience, convention only gets you so far. Creating route exceptions every time you can’t fit the convention feels dirty. Cramming everything into the parameters of the convention is equally as odoriferous.

Since I used the routeMissing method, I can then define any routes where I want to handle route parameters and everything keeps right on working. This is quite like creating a “Non-Resourceful Route” in Rails. I think that the Rails documentation puts it best when talking about forgoing convention.

“While you should usually use resourceful routing, there are still many places where the simpler routing is more appropriate. There’s no need to try to shoehorn every last piece of your application into a resourceful framework if that’s not a good fit.”

Source: http://guides.rubyonrails.org/routing.html#non-resourceful-routes

How Do You Do It?

How do you handle routing for your JavaScript applications? Do you define each and every route, or do you have a different strategy for handling convention so that your application can scale to 50 views without needing 50 routes?

We’re making great progress in JavaScript frameworks but there are still issues such as this one that need to be solved. This is becoming increasingly important as hybrid apps become more and more popular. Hybrid apps are ALL client, so having a proper framework to lean on is crucial. I have always liked the way Kendo UI Mobile handles this by loading views for you based on URLs. I would like to see desktop apps behave the same way.

14 comments"

  1. Nice post Burke! I have built several apps using this dynamic/convention based routing technique and feel that in many cases it really makes sense. It is nice to just add a new “view” and have it ready to go without adding any extra “plumbing”.

  2. Burke says:

    Thanks Ryan! This was something I had somehow not yet run into. I really think that if we’re going to call our frameworks MVC, then they should act at least a little bit more like actual MVC.

  3. Talysson says:

    Well, I use RhapsodyJS (http://rhapsodyjs.github.io/), it has convention-based routing out of box, it’s pretty easier, actually, config files for routes bother me…

    1. Burke says:

      I had not heard of that library. On first glance, it looks pretty compelling.

      1. Talysson says:

        It’s a new framework =)

  4. TJ VanToll says:

    I’ve actually written one large app that uses a convention very similar to what you describe in this article. In that app I actually took the extra step and placed the HTML for each view in the same directory structure. It’s nice to see a good writeup of this technique, and it’ll be interesting to see if future frameworks go down this route. (haha, route. Get it?)

  5. Per Ploug says:

    We built our angularjs app this way, wrote about our router setup back in February

    http://scriptogr.am/pploug/post/convention-based-routing-in-angularjs

  6. Hi Burke, I’ve been toying with this idea for quite some time. Attribute based routing is the ideal but it would be difficult to achieve in JS without scanning every file for the attributes. I typically see convention based routing used since it’s the intuitive solution. The tricky part is you end up with a fixed URL scheme and you’re stuck with 1 layout for all of your routes.

    I’ve been working on a new router concept that uses RequireJS to lazy-load views/controllers/whatever you call them. You map URLs to AMD module IDs. It includes support for path arguments and wildcards. Another big thing is it expects the view to require the layout. This lets different routes have different layouts and decouples the rendering from the router.

    The router itself doesn’t know about the MV* library or framework. Each library like Kendo UI would need a small amount of work in the ‘routeload’ event handler to render the view/layout and attach it to the DOM.

    You can check it out here:
    http://erikringsmuth.github.io/requirejs-router/

    1. Burke says:

      That’s quite interesting. What about optimization though? I sort of always operate under the assumption that all scripts will be minified with the optimizer.

  7. W. van Dam says:

    Nice idea, but doesn’t dynamically requiring files with requirejs break the optimisation step? Would it not be better to have a build step that generates a routes.js file with all routes based on the available files, methods, etc?

    btw. Saying that all JS frameworks before Angular claimed to be MVC is doing several frameworks injustice. For example, Knockout has always profiled itself as a MVVM framework and has been around quite a bit longer than Angular.

    1. The optimization piece is something that we handle as part of our build step (grunt) where we dynamically populate the “include” option for r.js in the grunt config. We do this both for dynamic modules and templates (text plugin) so that everything is in the optimized build.

      1. Burke says:

        Yep – huge omission on my part by not addressing that in the article. Thanks for bringing the issue up as I would not encourage production use of require without the optimizer.

  8. Andrew Betts says:

    I also hadn’t really considered the shortcomings of explicitly declared routes, and this makes a compelling case for convention based routing, but I also like the decoupling of routing from controllers, which is hard to achieve with attribute based routing. I like the feeling (even if maybe I’m kidding myself) that I could swap out the routing for a different routing mechanism and rewrite the URL structure to the controllers in a different way.

    1. Burke says:

      I “think” that URLs should dictate controller names and folder structure and this by and large works quite well. I’m still huge fan of attribute based routing since I feel like the convention only gets you so far before it starts making design decisions for you that don’t fit.

      Routing is hard.

Leave a Reply

Your email address will not be published. Required fields are marked *

© 2016 Modern Web & our authors. All rights reserved.