Routing Only AJAX Requests In RoR

Don’t Solve The Problem, Prevent It.

When you deal with input in programming, it is always a good practice to make sure you can’t get from the user what you wouldn’t want to get. I know it sounds strange at the beginning, but let me further explain with the simplest example:

Let’s say you want to get a date as an input from the user. A date can have many different formats and dealing with every possible format could become very frustrating. Now if you let the user enter the date in a simple text box you leave the power in the hands of the user. He decides how the input gets to your server, and you find yourself wasting a lot of code lines on the parsing of the date.

That is exactly the reason why graphical date picker components were created. A date picker lets the user choose from a graphical calendar and injects the formatted selection into the text box, which, obviously, should not be editable. So instead of allowing a broad number of date formats and dealing with the input in our code, we narrowed the input format to exactly what we want to get and we can deal now with business logic instead of date parsing. In other words, we did not solve the problem, we prevented it from ever happening. That’s a great attitude which will spare you a lot of code lines.

ActionDispatch and Problem Prevention

In this context, I wanted to talk about Rails routing a little bit. When we think about it, the ActionDispatch mechanism which resolves routes to controller actions is your (as a programmer) first encounter with user input. We already agreed it is best to prevent a problem, than resolving it in our code, so the routing mechanism is definitely the best place to start looking for code smells.

Let’s take a simple example in which we have a Post model with title, content, author_name and post_number attributes. We also want to let the user filter posts by each of these attributes at /posts/filter/content/<some_content_filter>. One might think of following solution :


# routes.rb
match 'posts/filter/:by_attribute/:value' => 'posts#filter'
# posts_controller.rb
class PostsController < ApplicationController
def filter
if ["title", "content", "author_name", "post_number"].include? params[:by_attribute]
@posts = Post.where(params[:by_attribute] => params[:value])
else
render :nothing => true, :status => :not_found
end
end
end

view raw

gistfile1.rb

hosted with ❤ by GitHub

Although this one will do the job, what we are doing here is solving the problem instead of preventing it. There is no reason, what so ever, to go through a full stack response and not halt the processing of the request the earliest possible – our routing definitions:


# routes.rb
match 'posts/filter/:by_attribute/:value' => 'posts#filter', :constraints => { :by_attribute => /^title$|^content$|^author_name$|^post_number$/ }
# posts_controller.rb
class PostsController < ApplicationController
def filter
@posts = Post.where(params[:by_attribute] => params[:value])
end
end

view raw

gistfile1.rb

hosted with ❤ by GitHub

This will respond with 404 not found to any filter which does not match the regular expression we defined. A request with an illegal filter won’t even reach our controller code – ActionDispatch deals with it a lot earlier. We benefit both in performance and in less code clutter in our controller.

Correctly Routing Only AJAX Requests

This was a basic example of using the routing mechanism in order to filter unwanted requests.
A more complicated one would be to make some controller actions respond only to AJAX requests. One way to do it would be the following:


# routes.rb
match 'posts/filter/:by_attribute/:value' => 'posts#filter'
# posts_controller.rb
class PostsController < ApplicationController
respond_to :js
def filter
@posts = Post.where(params[:by_attribute] => params[:value])
respond_with @posts
end
end

view raw

gistfile1.rb

hosted with ❤ by GitHub

respond_to :js and respond_with @posts make sure our controller will only reply to AJAX requests. I think you know where I’m going now – why not stop the request processing with a routing definition?
An ideal solution could be:


match 'posts/filter/:by_attribute/:value' => 'posts#filter', :constraints => { :only_ajax => true }

view raw

gistfile1.rb

hosted with ❤ by GitHub

Now there are some good news and some bad news regarding this code snippet:
The bad news: The :constraints hash does not have a :only_ajax => true option.
The good news: It is very easy to implement one yourself:


# routes.rb
class OnlyAjaxRequest
def matches?(request)
request.xhr?
end
end
match 'posts/filter/:by_attribute/:value' => 'posts#filter', :constraints => OnlyAjaxRequest.new

view raw

gistfile1.rb

hosted with ❤ by GitHub

All that is needed is an object which responds to the matches? method. The Request object is passed as a parameter to it so you can query it for whatever you’d like, including it being an xhr request using the xhr? method.

This way non-AJAX requests won’t even reach your controller, and you don’t have to deal with scary
respond_to do |format| blocks.

7 thoughts on “Routing Only AJAX Requests In RoR

  1. How about
    match ‘posts/filter/:by_attribute/:value’ => ‘posts#filter’,
    :constraints => -> (req) { req.xhr? }

    Don’t like to see non-route code into the routes file. :p
    And you are instantiating a new OnlyAjaxRequest class every time you declare a new full_ajax route, isn’t it ? Why not using Rails route helpers ?

    Thanks for the article, nice approach.
    :+1

    1. Thanks for the comment I have a few thoughts on it:

      1. I was actually not aware of the -> (req) { req.xhr? } syntax to filter only ajax requests. It is really cool thanks for mentioning this up.
      2. Regarding mixing non-route code: The right thing to do is to put this code under lib/routes or something like that and have all routes contraints in there.
      3. Also, if you find yourself repeating on that -> (req) { req.xhr? } I believe my approach is much more dry.
      4. I really think that my approach is more object oriented, readable and reusable than just putting this lambda syntax on the routes.rb file.

Leave a comment