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
Written by Ben Woodall
Related protips
2 Responses
Good article. Do you ever find a need to have shared validator logic between controllers? And if so, how do you approach that?
Great article, really clear and useful. Thanks!