Digging Deeper: Router Internals

In this section, we'll explore how Rails implements the beautiful routing DSL behind the scenes. This is going to be a bit technical, but nothing too complex, I promise.

Let's revisit the config/routes.rb file we saw earlier.

When you create a new Rails app, it automatically creates a /routes.rb file in the config directory. All it contains is a single call to the draw method on the object returned by the Rails.application.routes method. Additionally, it takes a block containing the routes for your application.

Rails.application.routes.draw do
  # application routes go here...
end

To understand how the above method works, we need to take a short detour and understand Ruby's instance_exec method defined on the BasicObject class.


Understanding How instance_exec Works

The instance_exec method takes a block and runs that block in the context of the object on which it's called. Inside the block, it sets self to that object, so you can access its instance variables and methods.

class Company
  def initialize(name, product)
    @name = name
    @product = product
  end

  def info
    "#{@name}: #{@product}"
  end
end

microsoft = Company.new 'Microsoft', 'Excel' 

microsoft.instance_exec do
  puts info    # access instance method
  puts @name   # access instance variable
end

# Output:

# Microsoft: Excel
# Microsoft

You might wonder what's the purpose of using instance_exec when you can just call the method directly on microsoft. You are, of course, right in this example, but, as we'll soon see, the most important benefit of instance_exec method is that it allows you to pass a block and run it later in the context of an object. This is really useful technique for creating special-purpose DSLs.

Not sure what I mean? let's revisit the routes.rb file.

Rails.application.routes.draw do
end

The Rails.application is an instance of the Application class which inherits from the Engine class. The Engine class has a routes method which returns an instance of the RouteSet class.

# railties/lib/rails/engine.rb

def routes(&block)
  @routes ||= ActionDispatch::Routing::RouteSet.new_with_config(config)

  # ...

  @routes
end

# Define the configuration object for the engine.
def config
  @config ||= Engine::Configuration.new(self.class.find_root(self.class.called_from))
end

It means that the block passed to the draw method in the routes.rb file is ultimately received by the draw method on an instance of the RouteSet class. This method passes that block to the eval_block method, as seen below.

# actionpack/lib/action_dispatch/routing/route_set.rb

def draw(&block)
  # ...
  eval_block(block)
  # ...
end

def eval_block(block)
  mapper = Mapper.new(self)

  mapper.instance_exec(&block)
end

As you can see, the eval_block method first creates an instance of the ActionDispatch::Routing::Mapper class and executes the block within the context of that instance.

What it means, is that any code we write inside the block passed to the Rails.application.routes.draw method will be evaluated as if it was written inside the Mapperclass.

If this is already started to get overwhelming and you feel like giving up, DON'T. Clone the Rails repo, open it in the editor, and trace the call stack with me. It's a fun exercise and you'll learn a lot along the way. If you want to learn how to read the Rails codebase without getting overwhelmed, check out: How I Read Rails Source Code

For example, the two pieces of code below are similar. However, the first version just reads better. It feels like a programming language specifically designed for the routing domain.

Rails.application.routes.draw do
  root 'application#index'
  get 'posts/publish', to: 'posts#publish'
end

# is similar to 

routes = ActionDispatch::Routing::RouteSet.new
mapper = Mapper.new(routes)
mapper.root 'application#index'
mapper.get 'posts/publish', to: 'posts#publish'

This also means that whenever you see a method in your routes file, you know where to look for its definition. It's the ActionDispatch::Routing::Mapper class and its included modules. For example, the commonly used get method is defined in the Mapper::HttpHelpers module.

def get(*args, &block)
  map_method(:get, args, &block)
end

This gives us a solid base to explore the Rails Routing API. We'll start our investigation with the match method.


If you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. You can also subscribe to my blog to receive future posts via email.