Non-trivial Rails 3.x routing

Disclaimer: There might be a better solution for this scenario. My expertise with Ruby and Rails is just 6 months. You're welcome to drop me a tweet or a message with better approaches.

At CartoDB, as one of the required steps for the 3.0 version we needed recently to change the URLs from the "classic" format of USER.cartodb.com/whatever to ORG.cartodb.com/u/USER/whatever .

This is a change that usually gives lots of headaches. At a previous job a similar change required a huge refactor of the MVC engine and hyperlink building system. At another was quicker but just because the only solution was to do an (ugly) regex hack deep inside the MVC framework.

Rails is initially all unicorns and rainbows. A magical routing system that allows to reduce written code, that maps automatically verbs to controller actions, that even differentiates between acting upon items or collections... A developer's heaven regarding MVC configuration ease.

Except that for advanced scenarios, this magic fades away and you need to fallback to more traditional and detailed config.

This is great for the majority of typical websites:

scope '/admin' do
resources :posts, :comments
end

But now imagine this new rules:

  • Any url might come, or might not, with an additional fragment, including a variable
  • This fragment might be optional, or might be mandatory

How do you specify an optional parameter at the Rails routes file? Like this:

get '(/u/:user_domain)/dashboard/tables' => ...

Looks easy... but remember that the param is optional. It might not be present... so we need to make sure it is always either sent or nil (but defined) so the code doesn't breaks. For this I implemented a before_filter at the base ApplicationController so it is always present.

Then, everything looked ready... until I checked the code and there was a myriad of links bult in all possible Rails ways: relative hardcoded, full path hardcoded, xxxx_path, xxxx_url, redirect_to xxxx, link_to xxxx...
And not only that, I also had to support that optional :user_domain parameter in most URLs, plus other ids or params sent like this sample route:

(/u/:user_domain)/dashboard/visualizations/shared/tag/:tag/:page

In the end, I decided to take the following new (and mandatory from now on) approach:

  • Full "literal-based" routes descriptions. They donĀ“t look fancy or like magic anymore but they are practical, work and anybody not even knowing Rails knows what it points to.
  • Always given a name/alias (" as xxxx"). So they can be called from views and controllers with _url/_path helpers without collisions, ambiguity or Rails magical way of autogenerating URLs (that has given some problems and for example don't allow parameters).
  • Every application link has to be built with _url/_path. No more handcrafted URLs.

This makes links to URLs a bit bigger than usual:

<%= link_to tag_name, public_tag_url(user_domain: params[:user_domain],tag: tag_name) %>

But we can be 100% sure that where that public_tag URL will go by giving a quick tool to routes file, we support the optional parameter, and we also ease any possible refactor in the future as searching for every link would be much easier than it was (it took 2 days and some bugfixes to uniformize the codebase).

About the "might be mandatory" part, what I did was adding another before_filter by default in the base Admin-scoped ApplicationController, and then disable it (with skip_before_filter) on those controllers were was optional.
Wherever it is mandatory, if the user is logged in and belongs to an organization, the app will redirect him to the organization-based URL. But for things like APIs we keep both the old and new format to preserve compatibility with existing third-party applications and projects.

Overall, I don't blame Rails for being so easy to reduce code. I understand that for the majority of scenarios it really eases code and speeds up this configurations, so nobody could have guessed this routing changes... But what it is a good practice is to be consistent and, even if you have 5 ways of generating urls/links, decide on using just one that is flexible enough for hypothetical future changes.

Magic has a problem: to use it, you need to be a mage. Now we're a team of (mostly) non-Ruby experts that needs to build lots of features and we cannot rely (at least on a short term) on everybody having deep knowledge of Rals, so we'll instead go more traditional ways but ease the first steps with the platform.

Comments?

Posted by Kartones on 2014-07-23