While reading the source code for the Maybe project (which is a really good Rails codebase that follows most of the Rails best practices and conventions), I came across this code in the router config file. It uses the Rails routing feature called direct
to create custom URL helpers for a polymorphic model.
direct :entry do |entry, options|
if entry.new_record?
route_for entry.entryable_name.pluralize, options
else
route_for entry.entryable_name, entry, options
end
end
At first glance, I didn't understand what it was doing, as I've personally never had to use the direct
method like this in my Rails projects so far, so decided to do some reading and wanted to share everything I learned.
First, what is the direct
method in the Rails router?
The direct
helper lets you defined custom named route helpers that generate URLs based on custom logic, rather than the default resourceful routes. You can then use these helpers throughout your application, just like any other URL helpers in Rails.
In the simplest case, you can simply return a URL that you'd like to use when that custom URL helper is used. Consider the following example.
direct :course_landing_page do
"https://writesoftwarewell.com/courses"
end
This will generate following URL helper methods
course_landing_page_url
returns https://writesoftwarewell.com/coursescourse_landing_page_path
returns/courses
.
If you pass any arguments to the helper, they get forwarded to the direct block.
direct :greeting_page do |user|
"https://writesoftwarewell.com/greet/#{user.name}"
end
This generates the helpers greeting_page_url
and greeting_page_path
. Assuming matz.name
returns "yukihiro"
, you can use them as follows:
greeting_page_url(matz)
returns https://writesoftwarewell.com/greet/yukihirogreeting_page_path(matz)
returnsgreet/yukihiro
.
To learn more about the basic usage of the direct method, check out the following post:

The direct
method is pretty flexible and you can encapsulate complex or polymorphic path logic in the block, which is what the Maybe router is doing.
direct :entry do |entry, options|
if entry.new_record?
route_for entry.entryable_name.pluralize, options
else
route_for entry.entryable_name, entry, options
end
end
This code defines a custom route helper called entry_path
and entry_url
. When you call entry_path(entry)
, where entry
can be a polymorphic object or a delegated type, Rails will use this block to figure out what URL path to return for the given entry
object.
Let's assume that Entry
is a polymorphic model and can be a Transaction
, Account
, or Trade
, etc.
What does the block do?
- If the entry is a new record, i.e.
entry.new_record?
returnstrue
, Rails will return the value generated by the following code:
route_for entry.entryable_name.pluralize, options
This means, generate the path for the pluralized entryable name, e.g. if entry.entryable_name
is "transaction", Rails will return the value of generated by calling route_for "transactions", options
which ultimately generates the path for the transactions resource, i.e. transactions_path
, which is /transactions
.
I'm guessing it's used for the "new" forms, where you want to POST to the collection.
Example:
entry = Entry.new(entryable: Transaction.new)
entry_path(entry)
# => transactions_path, i.e. "/transactions"
Since the entry was not saved, it routed to the collection path for the entryable type.
- If the entry is persisted (the else condition), it will return the value generated by this code:
route_for entry.entryable_name, entry, options
This means, generate the path for the singular entryable name and pass the entry as the argument to the helper, i.e. transaction_path(entry)
Example:
entry = Entry.find(1) # assume this entry is a transaction
entry_path(entry)
# => transaction_path(entry), i.e. "/transactions/1"
Since the entry was saved, it routed to the member path for the entryable type.
Why use this?
If you have a polymorphic model that can represent multiple types, a single helper can generate the correct path for any type.
In Maybe, an Entry
is a polymorphic model that can be one of several types, such as a Transaction
, Trade
, or Valuation
. The URL for an entry should depend on what kind of entry it is.
In addition, the logic forks based on whether the entry has been saved to the database. This is a common pattern in most web applications, where you have to use a different URL in forms, where a new object should POST to a collection URL /transactions
, while a form for an existing object should PATCH to a member URL, like /transactions/123
.
How to use it?
<%= link_to "View entry", entry_path(entry) %>
This will generate the correct path for the given entry, whether it's a new entry or a persisted entry, regardless of its underlying type.
The following table summarizes it.
Scenario | What gets called? | Example Output |
---|---|---|
New Transaction Entry | transactions_path(options) |
/transactions |
Existing Transaction | transaction_path(entry, options) |
/transactions/123 |
New Valuation Entry | valuations_path(options) |
/valuations |
Existing Valuation | valuation_path(entry, options) |
/valuations/456 |
Basically, you can use the direct helper method to handle URL generation for polymorphic types. Creating a single URL helper method that hides the complexity of figuring out the type of the entry and whether the entry is new or an existing record.
That's a wrap. I hope you found this article helpful and you learned something new.
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. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.