When to use Routing Constraints in Rails?
Constraint allows the router to behave differently based on the request at the routing level instead of controller level. eg showing a different homepage for different users or restricting URLs for some sub-domain.
Practical Example
Lets assume you are building a Rails application with authentication. When users sign up, you want to redirect to home page home#index
. The you realize you need a home page for signed in users and a different home page for guest user or unauthorized users
Solution 1
You could modify to HomeController
with a before_action
to redirect to DashBoardController
if user is signed_in
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# app/controllers/home_controller.rb
class HomeController < ApplicationController
before_action :redirect_to_dashboard_if_signed_in
def index
end
private
def redirect_to_dashboard_if_signed_in
# assume signed_in? commes for clearance ruby gem
if signed_in?
redirect_to dashboard_path
end
end
end
And in the config/routes.rb file
1
2
3
4
5
# config/routes.rb
Rails.application.routes.draw do
resource :dashboard, only: [:index]
resource :home, only: [:index]
end
This delegate routing decision to a controller.
But we can do better, using Routing constraints
Solution 2: Routing Constraints
But before that, lets dig a little bit of routing constraint constraints is
There are several ways to pass constraints to routes
- Segment Restriction
Involves using :constraints
option eg.
1
2
3
# config/routes.rb
match '/:year/:month/:day' => "info#about",
:constraints => { :year => /\d{4}/, :month => /\d{2}/, :month => /\d{2}/ }
With this constraint, visiting localhost:3000/foo/bar will raise a Routing Error, but localhost:3000/2020/05 will match the constraint and the request be handled successfully by info controller about action
- Request based constraint
Involves constraining routes based on any method of the request object
. For example based on subdomain or User-Agent or really any method. eg
1
2
3
# config/routes.rb
match '/:year/:month/:day' => "info#about",
:constraints => { user_agent: /Firefox/ }
This works only if the requesting agent matches matches Firefox. Other methods of usable in the Request object
include :subdomain
, :format
etc
- Dynamic Constraint
This uses matches?
method, either by using a lambda
or defining a class that has :matches?
as a class method
or an instance method
- Using
lambda
1
2
3
4
5
6
# config/routes.rb
get '*path',
to: 'proxy#index',
constraints: lambda {
|request| request.env['SERVER_NAME'].match('foo.bar')
}
Pass the request
object on the lamda as a params & you access all its methods
- Using a
Class
with aclass method
or aninstance method
- As an Instance method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# config/routes.rb
constraints(Subdomain.new) do
get '*path', to: "proxy#index"
end
# eg in lib/subdomain.rb
class Subdomain
def matches?(request)
(request.subdomain.present? &&
request.subdomain.start_with?('foobar')
)
end
end
-
- As a Class method
1
2
3
4
5
6
7
8
9
10
11
12
# config/routes.rb
constraints Subdomain do
get '*path', to: 'proxy#index'
end
class Subdomain
def self.matches?(request)
(request.subdomain.present? &&
request.subdomain.start_with?('foobar')
)
end
end
Our Example Use Case
We want to have a dashboard for guest users and logged in users. Here are the steps
- Configure config.routes.rb to
1
2
3
4
5
6
7
8
9
# config/routes.rb
Rails.application.routes.draw do
constraints SignedInHome.new do
root to: "dashboard#show", as: :dashboard
end
root to: "home#show"
end
Note: There are 2 routes for root, one within the constraint and one outside. The order is important to work.
Also we provide the dashboard home route a name dashboard_path
and dashboard_url
using as: :dashboard
option in the routes declaration.
-
Define
SignedInHome
class.
Let’s define the class inline but it can be created in the lib directory and be imported
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# config/routes.rb
Rails.application.routes.draw do
#....
end
class SignedInHome
def initialize(&block)
@block = block || lambda { |user| true }
end
def matches?(request)
@request = request
# logic to check if user is authenticated
# eg checking user_session is present and authenticated
user_session.present? && @block.call(current_user)
end
end
This code block is very basic, and assumes the user_session
is set and the route
within this constraint can respond to current_user
Resource
-
This blog is heavily borrowed from Thoughbot Clearance gem and specifically Clearance::Constraints::SignIn module
-
Also the Rails Documentation on Rails Routing From Inside