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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 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:

1 2 3 4 5 6 7 8 9 10 11 12 13 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.action_on_unpermitted_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:

1 2 3 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.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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:

1 2 3 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