Building VTS

The Official VTS Engineering Blog

Zen and the Art of Rails Routing Constraints

By Alex Wheeler

Rails is an amazingly powerful web framework that no doubt stands out as one of my favorite technologies I have the pleasure of working with every day as a software engineer at VTS. One of its greatest characteristics, and surely why it has gained so much traction over the last decade, is how dead simple it makes accomplishing certain tasks. While there are countless books that cover most of the things we love about Rails, something I’ve found myself appreciating recently is the concept of routing constraints, which are provided to us via the ActionDispatch::Routing::Mapper::Scoping module.

At a high-level Rails provides 3 types of constraints - HTTP verb constraints, segment constraints, and request-based constraints. We’ll start with the simplest - HTTP verb constraints. The match method commonly found in routes.rb takes a via option, which can be used to constrain multiple HTTP verbs to a given route.

Http Verb Constraints

1
2
#routes.rb
match 'properties', to: 'properties#index', via: [:get, :post]

In this case we’re routing /properties to our properties controller’s index action and constraining it to get and post requests. If we want to constraint it to every HTTP verb we could pass :all.

1
2
#routes.rb
match 'properties', to: 'properties#show', via: :all

This is pretty cool, but personally I don’t find myself using it too often, and rather go for using the resources method combined with the only option (to limit resourceful routing to specific CRUD operations/controller actions), but this is a whole different blog post on its own.

The next constraint on our list, the segment constraint, is a bit more interesting. Segment constraints allow us to enforce a format for a given parameter. The parameter must match the given format for the request in order to be fulfilled. Perhaps you only want to allow requests for properties with a sourcid following some format. This is super easy using segment constraints.

Segment Constraints

1
2
#routes.rb   
get 'properties/:slug', to: 'properties#show', constraints: { slug: /[a-z]+\.\d+/ }

In this case, a request to /properties/alex.9 would succeed, while a request to /properties/alex would 404.

The third constraint noted earlier, provided out-of-the-box by Rails, is the request-based constraint. With request-based constraints we can constrain a route using any method the Request object responds to - things like ip, user agent, subdomain, etc.

In my opinion this is one of the most useful types of constraints, purely because the request object responds to a whole lot of methods. A few examples might put things into perspective. As an application grows you find yourself building various segments of your application that own their own unique content and should probably live under their own subdomain. With request-based constraints we can easily constrain certain routes to a given subdomain.

Perhaps your development process encourages engineers to push recently completed features to a staging environment before deploying to production. This environment has an additional route that serves an awesome dashboard for viewing performance analytics related to the new feature. This could be easily constrained with a request-based constraint.

Request-Based Constraints

1
2
#routes.rb  
get 'dashboard', to: 'dashboard#index', constraints: { subdomain: 'staging' }

With this constraint, your application would serve the dashboard page from staging.yourapp.com/dashboard, but would reject requests to www.yourapp.com/dashboard.

Or maybe you want to serve a different home page to a user depending on the device they’re requesting the page from. Rails makes this dead simple.

1
2
3
#routes.rb
root 'iphone#index', constraints: lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }
root 'web#index'

In this case any request including a user agent header matching iPhone, will be fulfilled by the iphone controller’s index action, while any request not containing an iphone user agent will fall through and be routed to our web controller’s index action.

While these examples surely demonstrate the potential power and simplicity Rails provides through constraints it doesn’t answer one of the imho biggest factors when deciding to implement a new technology - flexibility. Software development is a world of constantly changing demands and the last thing a team needs is to be stuck in a corner because some uncompromising technology wasn’t flexible enough to adapt to an unforeseen problem. And what about complexity! I’m sure you can see how these constraints could become extremely verbose - and this whole time you’ve been told software development is all about limiting complexity. Well, lucky for us, I have two more things I’d like to briefly go over.

The constraints method can be called with a block containing all of the routes you’d like to constrain!

Say we’d like to constrain some basic CRUD operations on some resource to two subdomains, as well as a dashboard feature. Easy!

1
2
3
4
5
6
#routes.rb
constraints(subdomain: ["admin", "staging"]) do
  match 'properties', to: 'properties#show', via: :get
  match 'properties', to: 'properties#create' via: :post
  match 'dashboard', to: 'main#dashboard', via: :get
end

Epic. Now these routes can only be accessed from the admin and sandbox subdomains.

And this brings us to the last, and surely not least, point to address regarding the flexibility of routing constraints.

Custom Constraints

You can create your own custom constraints!

All you need to do is define a class with a matches? method, which returns true if a request should be given access to a route, or false if the request should be rejected. So if we want our properties route to only be accessible from our admin subdomain to users on iPhones then we could make a very simple custom class we’ll call IphoneAdmin and pass this in to the constraints method.

1
2
3
4
5
6
7
8
#iphone_admin.rb
class IphoneAdmin

  def self.matches?(request)
    request.env["HTTP_USER_AGENT"] =~ /iPhone/ && request.subdomain == "admin"
  end

end
1
2
3
4
#routes.rb
constraints(IphoneAdmin) do
  #your routes
end

For more information on the subject I always recommend the Rails api documentation as well as the Rails guides.

Feel free to reach out with any cool ways you’re using routing constraints! @askwheeler