Last Updated: November 09, 2021
·
28.89K
· benwoody

Validating REST queries with Rails

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

2 Responses
Add your response

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

over 1 year ago ·

Great article, really clear and useful. Thanks!

over 1 year ago ·