Model-Based Subdomain Routes for Rails

In my first article, I described a new rubygem for adding subdomain routing to Rails applications. In this second part, I’ll describe the use of the gem to key subdomain-based routes to your models.

Defining Model-Based Subdomain Routes

The idea here is to have the subdomain of the URL keyed to an ActiveRecord model. Let’s take a hypothetical example of a site which lists items under different categories, each category being represented under its own subdomain. Assume our Category model has a subdomain attribute which contains the category’s custom subdomain. In our routes we’ll still use the subdomain mapper, but instead of specifying one or more subdomains, we just specify a :model option:

ActionController::Routing::Routes.draw do |map|
  map.subdomain :model => :category do |category|
    category.resources :items
    ...
  end
end

As before, the namespace and name prefix for all the nested routes will default to the name of the model (you can override these in the options). The routes will match under any subdomain, and that subdomain will be passed to the controller in the params hash as params[:category_id]. For example, a GET request to dvds.example.com/items will go to the Category::ItemsController#index action with params[:category_id] set to "dvds".

Generating Model-Based Subdomain URLs

So how does URL generation work with these routes? That’s the best bit: just the same way as you’re used to! The routes are fully integrated with your named routes, as well as the form_for, redirect_to and polymorphic_path helpers. The only thing you have to do is make sure your model’s to_param returns the subdomain field for the user:

class Category < ActiveRecord::Base
  ...
  def to_param
    subdomain
  end
  ...
end

Now, in the above example, let’s say our site has dvd and cd categories, with subdomains dvds and cds. In our controller:

@dvd
=> #

@cd
=> #

# when the current host is dvds.example.com:
category_items_path(@dvd)
=> "/items"

polymorphic_path [ @dvd, @dvd.items.first ]
=> "/items/2"

category_items_path(@cd)
=> "http://cds.example.com/items"

polymorphic_path [ @cd, @cd.items.first ]
=> "http://cds.example.com/items/10"

As you can see, the first argument for the named routes (and polymorphic paths) feeds directly into the subdomain for the URL. No more passing :subdomain options. Nice!

ActiveRecord Validations

SubdomainRoutes also gives you a couple of utility validations for your ActiveRecord models:

  • validates_subdomain_format_of ensures a subdomain field uses only legal characters in an allowed format; and
  • validates_subdomain_not_reserved ensures the field does not take a value already in use by your fixed-subdomain routes.

(Undoubtedly, you’ll want to throw in a validates_uniqueness_of as well.)

Let’s take an example of a site where each user gets a dedicated subdomain. Validations for the subdomain attribute of the User model would be:

class User < ActiveRecord::Base
  ...
  validates_subdomain_format_of :subdomain
  validates_subdomain_not_reserved :subdomain
  validates_uniqueness_of :subdomain
  ...
end

The library currently uses a simple regexp to limit subdomains to lowercase alphanumeric characters and dashes (except on either end). If you want to conform more precisely to the URI specs, you can override the SubdomainRoutes.valid_subdomain? method and implement your own.

Using Fixed and Model-Based Subdomain Routes Together

Let’s try using fixed and model-based subdomain routes together. Say we want to reserve some subdomains (say support and admin) for administrative functions, with the remainder keyed to user accounts. Our routes:

ActionController::Routing::Routes.draw do |map|
  map.subdomain :support do |support|
    ...
  end

  map.subdomain :admin do |admin|
    ...
  end

  map.subdomain :model => :user do |user|
    ...
  end
end

These routes will co-exist quite happily together. We’ve made sure our static subdomain routes are listed first though, so that they get matched first. In the User model we’d add the validations above, which in this case would prevent users from taking www or support as a subdomain. (We could also validate for a minimum and maximum length using validates_length_of.)

Conclusion

So that’s about it for the SubdomainRoutes gem. Give it a go and let me know what you think. It offers a viable alternative to what’s out there at the moment, and some welcome additions. Code is here, if you want to check it out.