Collective Idea

Collective Idea Logo

Steve Richert

User-centric Routing in Rails 3

By Steve Richert on May 31, 2011 in rails and routes

Have you ever noticed that the GitHub homepage is different once you log in? I’m not talking about little changes here and there. It’s a completely different page. I have no idea how GitHub does this but I’ll venture a guess and demonstrate how to achieve the same effect.

Update: How to write user-centric routes in Rails 2

There are several common ways to pull this off but many miss the mark.

In the View

Switching content based on the current user is easy enough in the view. After all, current_user is available in the view so why not?

<% if current_user %>
<p>Welcome back, <%= current_user.name %>!</p>
<% else %>
<%= link_to "Learn about us!", about_path %>
<% end %>

But this falls apart when more user-specific information is needed in the view. In the case of GitHub’s user homepage, a lot of extra information (repositories, activity, etc.) is included. This information should be loaded up in the controller. So maybe that’s where the content switch belongs.

In the Controller

With the content switch in the controller, an action might look like this:

def home
  if current_user
    @feed_entries = current_user.feed_entries
    render :private_home
  else
    render :public_home
  end
end

This too feels just a little bit… off. The responsibility of a controller action is to load a dataset from the models and to render a template from the views. So if, depending on whether a user is logged in, an action loads up one of two datasets and renders one of two templates, shouldn’t that action be split into two actions? The question, then, is where do we switch between two actions?

In the Routes!

If you think about it, we do this all the time with REST. The same resource route can map to multiple actions depending the request’s HTTP method. A homepage could be mapped to two actions with:

root :to => "users#show", :via => :get
root :to => "users#create", :via => :post

But how can the routes access whether a user is logged in?

Rails has had routing constraints for some time but Rails 3 introduced request-based and advanced constraints. An advanced constraint is a custom object with a matches? method that receives the request as an argument.

In this case, I’m storing a unique user token in the cookies when a user is logged in. So in lib/logged_in_constraint.rb, I can write my constraint class to check for that token:

class LoggedInConstraint < Struct.new(:value)
  def matches?(request)
    request.cookies.key?("user_token") == value
  end
end

I also need to require the new constraint in config/routes.rb:

require File.expand_path("../../lib/logged_in_constraint", __FILE__)

The Fun Part

Now I can write my two new, GitHub-style homepage routes:

root :to => "static#home", :constraints => LoggedInConstraint.new(false)
root :to => "users#show", :constraints => LoggedInConstraint.new(true)

And that’s it! Give it a shot.

This is just one application of the new Rails 3 request-based constraints and there is no shortage of other fun and interesting applications:

root :to => "static#welcome_to_holland", :constraints => GeolocationConstraint.new("Holland, Michigan")
root :to => "static#visit_holland"

Enjoy!

By Steve Richert on May 31, 2011 in rails and routes

23 Comments

  1. Stephen

    Stephen May 31, 2011

    I tried to follow this but when I launch “rails s”, I get “uninitialized constant LoggedInConstraintn (NameError)”

  2. Mike Wyatt

    Mike Wyatt May 31, 2011

    wouldn’t it now be possible to open firebug/ developer tools, set a cookie with JavaScript and trick the app?

  3. Steve Richert

    Steve Richert May 31, 2011 http://collectiveidea.com

    Stephen: Good catch! My application includes lib in its autoload_paths. Otherwise, the lib/logged_in_constraint.rb file should be required in your routes. Post updated!

    Mike: Absolutely. This technique is meant only for smart routing, not secure authentication. If a cookie were forged, your authentication system should handle that in your users#show action.

  4. Mike Wyatt

    Mike Wyatt May 31, 2011

    just seems like a lot of baggage for what is basically

      def index    user_signed_in? ? _public : _private  end

  5. Zach Moazeni

    Zach Moazeni May 31, 2011 http://collectiveidea.com

    @Mike that works for a couple actions, but breaks down quickly when you have a number of them. Your example only renders views, and quite often your meaty actions will grow a lot larger when you support multiple user roles. Steve’s strategy is a lot more versatile than just an if/else.

  6. Evan

    Evan June 01, 2011

    Not directly related, but somewhat relevant: this is how I do the same thing with Devise & Warden:

    root :to => “users#dashboard”, :constraints => lambda { |r| r.env[“warden”].authenticate? }
    root :to => “home#index”

  7. Zach Moazeni

    Zach Moazeni June 01, 2011 http://collectiveidea.com

    @Evan Nice! I didn’t know you could pass a lambda to :constraints. A lot cleaner.

  8. Brandon

    Brandon June 01, 2011 http://extrast.com

    @Evan

    That is a great solution and really clean.  The only problem that I have with a lot of these solutions is that it really puts a lot of logic in routes.rb, whereas Steve’s solution keeps the logic separate.

  9. Rahul

    Rahul June 01, 2011

    Very well done, Thanks for sharing this.

  10. Behrang

    Behrang June 02, 2011 http://www.behrang.org/en/blog

    There’s something wrong about the JS for this page. I think too many events are fired when the content is scrolled up and down and as such it’s very sluggish.

  11. Jetsin

    Jetsin July 11, 2011 http://www.yahoo.com/

    I might be beiatng a dead horse, but thank you for posting this!

  12. Steve

    Steve July 17, 2011 http://steveklabnik.com

    This appears to be broken as of Rails 3.1.rc4. I get No route matches [GET] “/” when I add the constraint. Laaame.

  13. Chris

    Chris September 02, 2011 http://chris.com

    With Devise, same thing can be achieved with

    match ‘/dashboard’, :to => ‘User#dashboard’, :as => :user_root

  14. Seth Vargo

    Seth Vargo December 16, 2011 http://sethvargo.com

    For those of you using sessions instead of cookies, you can access

        request.session[:your_key]

    in a proc

  15. Marjan Povolni

    Marjan Povolni May 10, 2012 http://github.com/mamarjan

    I’m using omniauth with the identity strategy, and had to somewhat change the match? method with so it’s contents looks like this:

    request.env[“rack.session”].key?(“user_id”) == value

  16. Larry

    Larry May 17, 2012

    Thanks a lot for sharing. I build a constraint exactly like yours following your blog. It runs great in development, but when I run autotest, there’s a error raised: TypeError: superclass mismatch for class LoggedInConstraint

    Do you happen to know that? Thank you very much!

  17. Steve Richert

    Steve Richert May 18, 2012 http://collectiveidea.com

    @Larry, superclass mismatches occur when you try to open or define a class that’s already been defined with a different superclass. Because it’s only happening in your tests, you’re probably extending your constraint class in a test support file without properly specifying its superclass. Rather than opening the class, you could instead:

    LoggedInConstraint.class_eval do
      def matches?(request)
        # Overridden method for tests
      end
    end

    or:

    def LoggedInConstraint.matches?(request)
      # Overridden method for tests
    end
  18. Larry

    Larry June 04, 2012

    @Steve, Thanks for your responds Steve. I am still confused,  I didn’t write any test for class LoggedInConstraint in rspec. In whole projects there’s only in lib/ and in config/routes where LoggedInConstraint appears. Is there other causes to this problem? or..maybe I take your reply in a wrong way?

  19. Simon

    Simon September 13, 2012 http://polkaspots.com

    Just integrated this into our app. Much appreciated guys :)

  20. Stewart Johnson

    Stewart Johnson April 15, 2013 http://sprangles.com

    This was very helpful – thanks!

  21. sudheer

    sudheer October 21, 2013

    hi, am i need to write a Struct.rb class too, it is giving me wrong no.of arguments error.

  22. stephen

    stephen July 21, 2014 http://murdo.ch

    Rails 4 ain’t so hot on this technique, it complains that i’ve defined root twice

  23. mike

    mike October 16, 2014

    This technique works fine in Rails 4 but you need to add one thing. Rails 4 will complain that there are two root_paths. So just take one of them (I usually use the static landing page) and add ‘as: :landing’ to the end of it in routes.

    So something like:

    root :to => “static#home”, as: :landing, :constraints => LoggedInConstraint.new(false)
    root :to => “users#show”, :constraints => LoggedInConstraint.new(true)

Post a Comment

Contact Us

Find us on Google Maps
Collective Idea
44 East 8th Street, Suite 410
Holland, Michigan 49423 USA 42.790334-86.105251

Follow us on the Interwebs

We are currently available for medium and long term projects. Please get in touch if we can be of service.