Rails Routing Constraints

The Rails Guides talk about routing constraints in multiple contexts, but for the purposes of this article, we’ll focus on request based constraints. These, in their simplest form, allow us to match a route on the basis of some element of the request. Below (example taken directly from the Rails Guides) we see that a constraint is put on a route that requires the request to have a subdomain of ‘admin’ in order for the route to match.

get 'photos', to: 'photos#index', constraints: { subdomain: 'admin' }

So if I were to go to admin.example.com/photos, the route would match my request and the router would hand off the request to PhotosController index. If we used example.com/photos, the request would not match the constraint and the router would continue looking for a matching route further down the routes.rb file.

The constraint option on a route is not limited to a set number of configurations. We can pass it any ruby object that responds to a method called matches?. Based on the boolean return value of calling matches? on the ruby constraint object the request is matched to the route. The constraint can also be applied to a scoped or namespaced section of your routes, so any routes within the scope will first need to match the constraint.

At ClubZap we use constraints to handle API versioning (we have a constraint for each version that determines the version from the incoming request object) as well as our custom domain logic. As part of our offering to clubs, we can provide a ClubZap powered website. And with website hosting comes the requirement to handle custom domains. Our constraint object in this instance interogates the incoming request host and matches the club website routing if we confirm that the host is in fact a custom domain belonging to one of the clubs using our product. This is pretty much all the logic we run in order to determine if the incoming request is for one of our customer’s domains.

class CustomDomainConstraint
  def matches?(request)
    # handle the case of the incoming host being either 
    # the www. subdomain or a root domain
    host = if request.subdomain == 'www'
             request.host[4..]
           else
             request.host
           end

    WebsiteService.site_exists?(domain: host)
  end
end

As you can see, we first strip off any www so we only have to deal with the root domain. All we need to do then is return a boolean at the end of the matches? method. Our WebsiteService takes care of that by querying our table of custom websites for the matching domain. We use this constraint to match a scoped module within our routes file and direct all requests to the relevant controllers that handle the custom website logic and rendering.

scope module: :public, constraints: CustomDomainConstraint.new do
  root 'front_pages#home', as: 'custom_domain_root'
  resources :articles, only: %i[index show]
end

This is a great example of a brief foray outside of the standard Rails configuration. We get all of the simplified base logic but when we need to do something a little off menu, the capability is there for us to extend at will.

Published 18 Aug 2018

I help run a startup called ClubZap. We help many of the biggest grass roots clubs in Ireland and the UK to communicate with their members and collect online payments.
Aidan Quilligan on Twitter