Title: Non-trivial Rails 3.x routing
Slug: non-trivial-rails-3-x-routing
Date: 2014-07-23 20:45:00
Author: Kartones
Lang: en
Tags: Development, Ruby, Troubleshooting
Description: Challenges and solutions encountered while changing URL formats in Ruby on Rails.

 <p>Disclaimer: There might be a better solution for this scenario. My expertise with Ruby and Rails is just 6 months. You're welcome to <a href="https://blog.kartones.net/page/contact/">write to me</a> with better approaches.</p> <p> </p> <p>At <a href="http://www.cartodb.com">CartoDB</a>, as one of the required steps for the 3.0 version we needed recently to change the URLs from the "classic" format of <font face="Courier New">USER.cartodb.com/whatever</font> to <font face="Courier New">ORG.cartodb.com/u/USER/whatever</font> .</p> <p>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.</p> <p>Rails is initially all unicorns and rainbows. A magical <a href="http://guides.rubyonrails.org/routing.html">routing system</a> 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.</p> <p>Except that for advanced scenarios, this magic fades away and you need to fallback to more traditional and detailed config.</p> <p>This is great for the majority of typical websites:</p> <p><font face="Courier New">scope '/admin' do<br>  resources :posts, :comments<br>end</font></p> <p> </p> <p>But now imagine this new rules:</p> <ul> <li>Any url might come, or might not, with an additional fragment, including a variable </li><li>This fragment might be optional, or might be mandatory</li></ul> <p> </p> <p>How do you specify an optional parameter at the Rails routes file? Like <a href="https://github.com/CartoDB/cartodb/blob/master/config/routes.rb#L40">this</a>:</p> <p><font face="Courier New">get '<strong>(/u/:user_domain)</strong>/dashboard/tables' =&gt; ...</font></p> <p> </p> <p>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 <font face="Courier New">nil</font> (but defined) so the code doesn't breaks. For this I implemented a <font face="Courier New">before_filter</font> at the <a href="https://github.com/CartoDB/cartodb/blob/master/app/controllers/application_controller.rb#L149">base ApplicationController</a> so it is always present.</p> <p> </p> <p>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, <font face="Courier New">xxxx_path</font>, <font face="Courier New">xxxx_url</font>, <font face="Courier New">redirect_to xxxx</font>, <font face="Courier New">link_to xxxx</font>...<br>And not only that, I also had to support that optional <font face="Courier New">:user_domain</font> parameter in most URLs, plus other ids or params sent like this sample route:</p> <p><font face="Courier New">(/u/:user_domain)/dashboard/visualizations/shared/tag/:tag/:page</font></p> <p> </p> <p>In the end, I decided to take the following new (and mandatory from now on) approach:</p> <ul> <li>Full "literal-based" routes descriptions. They <a href="https://github.com/CartoDB/cartodb/blob/master/config/routes.rb">don't look fancy or like magic anymore</a> but they are practical, work and anybody not even knowing Rails knows what it points to.  </li><li>Always given a name/alias ("<font face="Courier New"> as xxxx</font>"). 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).  </li><li>Every application link has to be built with _url/_path. No more handcrafted URLs.</li></ul> <p>This makes links to URLs a bit bigger than usual:</p> <p><font face="Courier New">&lt;%= link_to tag_name, public_tag_url(user_domain: params[:user_domain],tag: tag_name) %&gt;</font></p> <p> </p> <p>But we can be 100% sure that where that <font face="Courier New">public_tag</font> 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).</p> <p> </p> <p>About the "might be mandatory" part, what I did was adding another <font face="Courier New">before_filter</font> by default in the base Admin-scoped <font face="Courier New">ApplicationController</font>, and then disable it (with <font face="Courier New">skip_before_filter</font>) on those controllers were was optional. <br>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.</p> <p> </p> <p> </p> <p>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. </p> <p> </p> <p>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.</p>
