TL;DR: Use the resource itself instead of a route helper when representing urls in your Rails application. Route helpers hard code knowledge of model names, making refactoring to polymorphism harder.
This is a story about a widely used, and potentially very brittle Rails feature.
We have a large Rails application. Our large Rails application has a lot of views. Those views have lots of links and forms. Many of our controller actions have urls in them (for redirection, typically).
When we represented a URL in the app, we often used a rails route helper. For example, imagine we had the following routes:
namespace :admin do scope module: “admin” do resources :widgets do scope module: “widgets” do resources :gizmos resources :things resources :gadgets end end end end
Our app has an admin section. In the admin section, you can manage widgets. Also, widgets have gizmos.
When we create a gizmo for a widget, we want to redirect back to the widget gizmos page, to view all the gizmos for the widget we’re managing.
def create @widget = Widget.find params[:widget_id] @gizmos = @widget.gizmos.create params[:gizmo] respond_with @gizmo, location: admin_widget_gizmos_path(@widget) end
respond_with will redirect to the show page for the widget on success by default, we have to override the location to redirect to on success by passing the
location: admin_widget_gizmos_path(@widget) to it.
Widgets are a central concept in our app. They’re EVERYWHERE. And at some point, they became polymorphic. We suddenly needed to differentiate between fizzy widgets and fuzzy widgets. They had similar interfaces, but different behaviors. They were also presented differently to users. They had some shared nested resources, and they each had some of their own separate nested resources.
So we separated the routes where that made sense. Routes in our application now looked like:
namespace :admin do scope module: “admin” do resources :fizzy_widgets do scope module: “fizzy_widgets” do resources :gizmos end end resources :fuzzy_widgets do scope module: “fuzzy_widgets” do resources :gadgets end end resources :widgets do scope module: “widgets” do resources :things end end end end
Oh noes! Now all of our
admin_widget_gizmo*_path helpers don’t exist anymore!
Do we have to switch them all over to
admin_fizzy_widget_gizmo*_path helpers? No! For pretty much everything in Rails that expects a url, you can instead pass an object (or an array of objects). For example, let’s head back to our controller action:
def create @widget = Widget.find params[:widget_id] @gizmos = @widget.gizmos.create params[:gizmo] respond_with @gizmo, location: [:admin, @widget, :gizmos] end
We’ve replaced our hardcoded path
admin_widget_gizmos_path(@widget) with our resource itself:
[:admin, @widget, :gizmos]. Internally, rails will convert this to a call to
admin_specific_widget_type_gizmos_path. Most (if not all) rails methods that expect a path will also take an object representation of your resource, like
Route helpers have hardcoded knowledge of types, and make refactoring resources with polymorphism harder. If you have no reason to hard wire the knowledge of your resource types around your application, then use the object representation of your resource instead.
Note: If we hadn’t needed to vary the presentation of our polymorphic widgets, we could have left the routes alone and instead made the “model_name” method for each polymorphic widget return the same
See ActiveModel::Naming for more.
About the Author
Matt Parker is Head of Engineering for Pivotal LabsMore Content by Matthew Parker