Saturday, April 27, 2013

Managing Rails Routes When They Get Out Of Hand

Rails developers have gotten really good over the years in breaking down logic in beefy controllers to business domain logic that lives in Models and some reusable control logic that lives in Controller Modules. Not only does this address cohesion concerns, but also the lines per file recommended limit of no more than say 200 in Ruby or 400 max.

However, what I have not seen done often enough in most of the projects I've worked on is handling of these concerns in the Rails Routes file "routes.rb". I've often seen gigantic routes files that not only take a long time to detangle and understand, but also aren't organized in any fashion as to the grouping of routes per business domain area (e.g. Feature A routes, Feature B routes, etc...), which brings us to the topic of this blog post. :)

If you simply follow the software engineering recommendation to break files into smaller ones once they've surpassed a limit of say 400 lines, then a route file (typically 500+ on bigger Rails projects and sometimes 1000+ or even 2000+) is naturally to be broken into multiple route files.

There are several techniques for doing it, but here is one easy technique that I've used on the last couple of Rails projects I was on:

1. Break routes.rb along these lines: Static Routes, Admin Routes, Account Routes (e.g. authentication with devise), and Per Feature Routes (e.g. Company Management Routes, User Personalized Info Routes, etc...)

2. Create directory "config/routes" to store smaller route files

3. Create a route file under "config/routes" for each one of the functional areas mentioned in Step 1, named "XYZRoutes" and holding a Ruby module like the following example:

module Routes
  module AccountRoutes
    def self.draw(context)
      context.instance_eval do
        devise_for :users, :controllers => { :registrations => "registrations", :sessions => 'sessions' }
        devise_scope :user do
          get "/login" => "sessions#new"
          delete '/logout' => 'sessions#destroy'
          get '/logout' => 'sessions#destroy'
          get '/register' => 'registrations#new'
        end
      end
    end
  end
end


4. Update routes.rb to look like this:


Dir.glob("#{Rails.root.to_s}/config/routes/**/*.rb").each {|route_file| load(route_file)}

SomeApp::Application.routes.draw do
  Routes::StaticRoutes.draw(self)
  Routes::AdminRoutes.draw(self)
  Routes::CompanyRoutes.draw(self)
  Routes::AccountRoutes.draw(self)
  Routes::UserRoutes.draw(self)

  mount JasmineRails::Engine => "/specs" if defined?(JasmineRails)

  root :to => "home#index"
end

This should provide you with a good blueprint for how to better organize routes files in Rails.

To summarize, break down routes.rb in Rails when it gets over 400 lines of code for the following benefits:

  • Higher cohesion per route file, improving general understanding of that area's focus as well as ease of maintainability by allowing you to find URLs for a particular domain area faster.
  • Better management of route content by not having to scroll through 100s of lines of code (sometimes 1000s) to find the URL that you want to change

I've worked in an environment once where the development team wrote an internal library to streamline modularization of routes.rb. If you know of a public open source one, please mention in comments.

2022-03-01 Update:

Rails now supports breaking up route files natively through the `draw` macro:

https://guides.rubyonrails.org/routing.html#breaking-up-very-large-route-file-into-multiple-small-ones

# config/routes.rb

Rails.application.routes.draw do
  get 'foo', to: 'foo#bar'

  draw(:admin) # Will load another route file located in `config/routes/admin.rb`
end

# config/routes/admin.rb

namespace :admin do
  resources :comments
end

This should have been mentioned a while ago, but better late than never I guess.

No comments: