Custom URL Helpers with the direct Method in Rails Router

Custom URL Helpers in Rails with the direct Method

This is the first post in the Campfire deep dive series where we explore the first ONCE product from 37signals to learn and extract useful patterns, ideas, and best practices. This post explores the direct method in the Rails Router that lets you define custom URL helpers for your application.

6 min read

I recently bought 37signals' first ONCE product, Campfire. I think it's a fantastic deal to get to read the source code of a Rails app running in production, from a world-class Rails development team and DHH himself. I also attended a code walkthrough with David on Friday, and seeing his energy and passion about the nuts and bolts of the code alone was worth the entry price of $299.

💥
So I’ve been sweating those details a lot with ONCE #1. Regularly reading through the entire 7,412 lines of code to polish spacing, pick slightly better model names, and correct cohesion issues. And every time I do, I usually also find other small quality issues that’ll lead to a better product which fewer people will have issues with.

- David Heinemeier Hansson, in Patek levels of finishing

I don't plan to use the chat application right away, and haven't even installed it yet, as I've been busy digging into the source code for the past few days, which is incredibly simple yet powerful and very, very expressive.

One of the very first things I do whenever I come across a new Rails codebase is to open the routes file and see what are the points of interaction (i.e. route endpoints) with the application.

Routing is such an important concept in Rails that I wrote a ~5000 word post on it: Understanding the Rails Router: Why, What, and How.

Understanding the Rails Router: Why, What, and How
The router is the entry point of your Rails application. It acts as the gatekeeper for all incoming HTTP requests, inspecting and sending them to a controller action; even filtering and rejecting them if necessary. In this article, we’ll do a deep dive into the Rails Router to understand it better.

Anyway, while reading Campfire's routes.rb file, I came across a new method called direct that I'd never seen before.

# config/routes.rb

direct :fresh_user_avatar do |user, options|
  route_for :user_avatar, user, v: user.updated_at.to_fs(:number)
end

direct :fresh_account_logo do |options|
  route_for :account_logo, v: Current.account&.updated_at&.to_fs(:number)
end

You may have seen and used this method before, but I'm sure there're many like me who don't know what it does and have never used it before. So, in this post, we'll take a look at this direct method. What is it, and what does it do?

Let's dig in.


Named Routes and Route Helpers

You know that whenever you create standard routes, Rails provides two helper methods such as xxx_path and xxx_url which generate the corresponding URL for that route, using the provided parameters. For example,

  • photos_path returns /photos
  • new_photo_path returns /photos/new
  • edit_photo_path(:id) returns /photos/:id/edit (e.g. edit_photo_path(10) returns /photos/10/edit)

In addition, you can pass the :as option to a route to give a specific name for the route. When you name a route, Rails will automatically generate helper methods to generate URLs for that route. These methods are called name_url or name_path, where name is the value you gave to the :as option.

For example, the following route definition will generate logout_path and logout_url helper methods that return /exit and https://localhost:3000/exit respectively.

get 'exit', to: 'sessions#destroy', as: :logout

With that background, let's try to understand the direct method.

What is the direct Method in the Rails Router?

The direct method lets you define custom URL helpers. You define the helper name first, and then define the URL that this helper will return when called. Any parameters passed to the helper method are directly passed to the block.

direct :course_landing_page do
  "https://courses.writesoftwarewell.com/"
end

# >> course_landing_page_url
# => "https://courses.writesoftwarewell.com/"

direct :greeting_page do |user|
  "https://writesoftwarewell.com/#{user.name}"
end

# >> greeting_page_path(matz)
# => "/yukihiro"

Using the direct method, you can override and/or replace the default behavior of routing helpers in Rails.

The above example was very simple: just define the helper name and return the URL. However, as with everything in Rails, this simple API is also very flexible. In addition to the string URL, which is treated as a generated URL, you can also return one of the following options, for more powerful customizations:

  1. A Hash, e.g. { controller: "page", action: "index" }
  2. An Array, which is passed to polymorphic_url
  3. An Active Model instance, or
  4. An Active Model class

In fact, the return value of the block must be a valid argument for the url_for method. This method actually builds the URL string.

direct :previewable do |file|
  [ file, slug: file.slug ]
end

direct :sales do
  { controller: 'LandingPages', action: 'sales', subdomain: 'pay' }
end

To learn more about how you can use the above options, I suggest reading the documentation for the url_for method.

What's more, you can even call other URL helpers. However, an important thing to keep in mind is not to call the same custom URL helper you're defining. Otherwise, it will keep calling itself, resulting in the notorious Stack Overflow error.

direct :course_landing_page do
  # Warning: Don't Do This!!!
  course_landing_page_path
end

Behind the Scenes: Reading the Source

As always, let's open the Rails source code and see how this method is implemented. Surprisingly, it's not long, just four lines of code:

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

def direct(name, options = {}, &block)
  unless @scope.root?
    raise RuntimeError, "The direct method can't be used inside a routes scope block"
  end
  
  @set.add_url_helper(name, options, &block)
end

The very first thing we notice is that the direct method cannot be used inside of a scope block such as namespace or scope. If you do it anyway, Rails will raise an error.

Finally, direct delegates to the add_url_helper method to create a custom URL helper, which is an internal method. It defines the name_path and name_url helpers, given a name.

If you're interested in learning about the internals of the Rails Router, check out the following section: How Rails implements the beautiful routing DSL.

Revisiting Campfire

Let's get back to the two Campfire routes we saw earlier. With everything we've just learned, can you try to guess what these routes do and in what context they can be used?

# config/routes.rb

direct :fresh_user_avatar do |user, options|
  route_for :user_avatar, user, v: user.updated_at.to_fs(:number)
end

direct :fresh_account_logo do |options|
  route_for :account_logo, v: Current.account&.updated_at&.to_fs(:number)
end

Let's take the first route. It defines two helper methods named fresh_user_avatar_path and fresh_user_avatar_url. In addition, it accepts two parameters, user and options.

Inside the block, it calls the route_for method. Let's inspect the Rails source to see what this method does:

# action_dispatch/routing/url_for.rb

def route_for(name, *args)
  public_send(:"#{name}_url", *args)
end

It simply calls the helper method, passing any arguments passed to it. So our helper fresh_user_avatar_url is indirectly calling the user_avatar_url helper. The route for this helper is defined just a few lines above:

resources :users, only: :show do
  scope module: "users" do
    resource :avatar, only: %i[ show destroy ]
  end
end

Here's an example where it's used in the codebase, to display the user's avatar:

# app/views/users/show.html.erb

<%= image_tag fresh_user_avatar_path(@user), role: "presentation", class: "avatar" %>

And that's how Campfire uses the direct method in the Rails router to create a custom helper that returns the user's avatar URL.

P.S.: I still haven't figured out the purpose of the option passed to the helper, v: user.updated_at.to_fs(:number) as I just couldn't find out where it's being used in the codebase. If any 37signals developers are reading this post and know the answer, please enlighten me.

💡
Update: The v option is used for invalidating the cached image and fetching the latest version of the avatar. Whenever the user updates their profile picture, the generated URL will be different, which will bust the cache and return a fresh copy of the avatar.

That's a wrap. I hope you found this article helpful and you learned something new.

💡
By the way, even though I said I don't plan to use Campfire right away, I might use it someday on this blog itself, as a simple community forum at chat.writesoftwarewell.com - so that I could chat with you all awesome software writers. We've a pretty sizeable community of Rails developers here and I think some may find it useful. (Let me know if you'd be interested in something like that.)

As always, 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. I reply to all emails I get from developers, and I look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.