Where developers come to connect, share, build and be inspired.

5

Validating REST queries with Rails

2968 views


I've recently been working on a few RESTful API's using Rails. One of the problems that I keep seeing with end users is that they usually don't read the documentation very well and make simple mistakes when making specific requests and queries. This is easily solved with error handling and validation of the API. There are a few gems out there that will handle this sort of situation for you, but there's already so much in Rails to help you get this done out of the box.

First off, I like keeping my query validations separate by putting them into app/controllers/validates and then including them at startup. Each controller should then have their own validator.

Validating specific queries can be done with ActionController::Parameters. By creating an ActionController::Parameters object and passing in the request params, we can then #permit specific queries to be made.

For example, let's say you're building a location aware app that needs to query via latitude and longitude. You can build an ActionController::Parameters object to permit only the ?latitude and ?longitude queries:

module Validate
  class Location

    attr_accessor :latitude, :longitude

    def initialize(params={})
      @latitude  = params[:latitude]
      @longitude = params[:longitude]
      ActionController::Parameters.new(params).permit(:latitude,:longitude)
    end

  end
end

If a query other than ?latitude=X&longitude=Y are passed, a ActionController::UnpermittedParameters exception is returned. We'll need to rescue_from this exception in order to return our error to the user. Our Locations Controller would look something like:

class LocationsController < ApplicationController

  def index
  end

  ActionController::Parameters.action_on_unpermitted_parameters = :raise

  rescue_from(ActionController::UnpermittedParameters) do |pme|
    render json: { error:  { unknown_parameters: pme.params } }, 
               status: :bad_request
  end

end

ActionController::Parameters.actiononunpermitted_parameters is set to :raise so that errors are thrown instead of logged.

Now, a query such as /locations?latitude=47.60&longitude=-122.33 would return a valid location for us, but a query containing anything else would return an error. For example:

GET /locations?query=bad

{"error":{"unknown_parameters":["query"]}}

So that's pretty awesome. Now end users of the API will know they can only use specific queries. But which queries? It'd be nice if there was a way to let an end user know which queries were needed. If only Rails had a way to validate certain values...

Enter ActiveModel::Validations. The same validations we use on our ActiveRecord Models can be used to validate queries we make to the API.

To do this, we need to include ActiveRecord::Validations in our API validations. Then, you can validate queries just like you validate models.

module Validate
  class Activity
    include ActiveModel::Validations

    attr_accessor :latitude, :longitude

    validates :latitude, presence: true, numericality: true
    validates :longitude, presence: true, numericality: true

    def initialize(params={})
      @latitude  = params[:latitude]
      @longitude = params[:longitude]
      ActionController::Parameters.new(params).permit(:latitude,:longitude)
    end

  end
end

Not only can we validate the presence of a certain query, we can validate that it must be in a certain format, contain certain items, etc... Anything you can validate with ActiveModel::Validations you can now validate in your API.

A few changes need to be made in our Locations Controller in order to make this work:

class LocationsController < ApplicationController
  before_action :validate_params

  def index
  end

  rescue_from(ActionController::UnpermittedParameters) do |pme|
    render json: { error:  { unknown_parameters: pme.params } }, 
               status: :bad_request
  end

  private

      def validate_params
        location = Validate::Location.new(params)
        if !location.valid?
          render json: { error: location.errors } and return
        end
      end

end

Now we get the following errors:

GET /locations?latitude=near&longitude=far

{"error":{"latitude":["is not a number"],"longitude":["is not a number"]}}

There's so much more that you can do with this. I've built a demo app and put it on Github to show off the usage described in this post. Check it out @ https://github.com/benwoody/validate_params

Comments

  • None

    Good article. Do you ever find a need to have shared validator logic between controllers? And if so, how do you approach that?

  • None

    Great article, really clear and useful. Thanks!

Add a comment