DEV Community

Buddy Reno
Buddy Reno

Posted on

Organizing Routes In Rails

I love Ruby on Rails, but one thing that bothered me a lot was how quickly the routes.rb file can get unwieldy. When I began creating version 2 of an API I was making, this issue presented itself. Thankfully, there's an easy way to handle this with a little knowledge of how ruby can reference extended objects.

Consider the following example routes file:

Rails.application.routes.draw do
  root to: redirect("https://www.example.com/show")

  get "/ready", to: proc { [200, {}, [""]] }


  namespace :api do
    namespace :v1 do
      # about 31 routes
    end
    namespace :v2 do
      # about 36 routes
    end
    # v3, v4, v5 and so on
  end
end
Enter fullscreen mode Exit fullscreen mode

That can get extremely long and drawn out, especially if you can't drop older versions of the API for an extended period of time. We can break this up easily by utilizing some events and methods of ruby's extend class and break these versioned routes into their own files.

Folder and File Structure

The routes.rb file is located in the config folder of a Rails app. Create a new folder in the same place: config/routes. We'll store the new route extensions here.

Inside that folder, I want to name the file the same thing as the namespace that will be in the file. The first route collection I want to siphon off is :api :v1. Name the file api_v1_routes.rb to match our module and namespace structure.

Open the new file and create a module called ApiV1Routes.

module ApiV1Routes
end
Enter fullscreen mode Exit fullscreen mode

Extended

When you extend an object, ruby has a helpful event called extended that allows you to execute some code after the object is... extended.

module ApiV1Routes
  def self.extended(router)
  end
end
Enter fullscreen mode Exit fullscreen mode

In the above method, when we create a class that extends this particular module, the extended event will fire with a reference to the object that is being extended. In our case, the extended method is going to receive the router as a parameter.

Execution In Context

Now we have a module that can get a reference to the rails router when it's extended, how do we add routes to the router from here?

All objects in ruby have a method called instance_exec. This takes a block and executes the block within the context of the caller. In our ApiV1Routes module, this looks like the following.

module ApiV1Routes
  def self.extended(router)
    router.instance_exec do

      # add routes
      namespace :api do
        namespace :v1 do
          # 31 v1 routes
        end
      end

    end
  end
end
Enter fullscreen mode Exit fullscreen mode

According to the documentation for instance_exec, the self is set to the calling object so we have access to all the object's instance variables and methods.

In order to set the context, the variable self is set to obj while the code is executing, giving the code access to obj’s instance variables.

The routes are being added as if we were within the routes.rb file itself!

Tying it all together.

Now that we've created the module we need rails to load these route modules, otherwise we won't be able to reference them in our code.

Open the application.rb file, which should also be located in the config folder. Add the following line of code:

# Load split router files
config.autoload_paths += %W(#{config.root}/config/routes)
Enter fullscreen mode Exit fullscreen mode

This tells rails to load the files in the routes folder, since config/routes isn't a default that rails will autoload for you.

Back in the routes.rb file, at the top add extend ApiV1Routes before everything else in the do block.

Rails.application.routes.draw do
  extend ApiV1Routes
  extend ApiV2Routes

  # all our other routes
end
Enter fullscreen mode Exit fullscreen mode

That's it! Any routes you've added this way should now be able to be used! Repeat this for any clearly defined route structures you'd like to separate, then add the extend ModuleName to the routes.rb file.

Top comments (5)

Collapse
 
andrewbrown profile image
Andrew Brown 🇨🇦 • Edited

Here's an alternate way.
No need to use extends or modularize your routes.

# config/application.rb
module MyApp
  class Application < Rails::Application
    config.paths['config/routes.rb'].concat Dir[Rails.root.join("config/routes/*.rb")]
  end
end
Enter fullscreen mode Exit fullscreen mode
# config/routes.rb
MyApp::Application.routes.draw do
  namespace :api do
    resources :users
    resources :projects
    resources :tasks
  end
end
Enter fullscreen mode Exit fullscreen mode
# config/routes/admin.rb
MyApp::Application.routes.draw do
  namespace :admin do
    resources :users
    resources :projects
    resources :tasks
  end
end
Enter fullscreen mode Exit fullscreen mode
# config/routes/admin_api.rb
MyApp::Application.routes.draw do
  namespace :admin do
    namespace :api do
      resources :users
      resources :projects
      resources :tasks
    end
  end
Enter fullscreen mode Exit fullscreen mode
# config/routes/api.rb
MyApp::Application.routes.draw do
  namespace :api do
    resources :users
    resources :projects
    resources :tasks
  end
end
Enter fullscreen mode Exit fullscreen mode
Collapse
 
vickodin profile image
Victor

Thanks, used your way.

P.S. In previous project used the next receipt = mattboldt.com/separate-rails-route... but your style is more clean, imho.

Collapse
 
buddyreno profile image
Buddy Reno

Dude that's great! I will re-evaluate my approach knowing this. :)

Collapse
 
radinreth profile image
radin reth

FYI : Now rails v6.1 support out of the box,

guides.rubyonrails.org/routing.htm...

Collapse
 
vickodin profile image
Victor

Thanks!