labs

Rails route helpers are brittle

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

Since 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 respond_with, link_to, render, redirect_to, form_for, etc.

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 ActiveModel::Name.
See ActiveModel::Naming for more.