EightyEight EightyEight - 28 days ago 5
reST (reStructuredText) Question

Transforming ActiveRecord validation errors into API consumable errors

I'm writing a pretty standard CRUD RESTful API in Rails 4. I'm coming up short on error handling though.

Imagine I have the following model:

class Book < ActiveRecord::Base
validates :title, presence: true
end


If I try to create a book object without a title I'll get the following error:

{
"title": [
"can't be blank"
]
}


ActiveRecord validations are designed to be used with Forms. Ideally I'd like to match up each human readable validation error with a constant that can be used by an API consumer. So something like:

{
"title": [
"can't be blank"
],
"error_code": "TITLE_ERROR"
}


This can be both used to display user facing errors ("title can't be blank") and can be used within other code (
if response.error_code === TITLE_ERROR
...). Is there any tooling for this in Rails?

EDIT: Here's a very similar question from Rails 2 days.

Answer

On error_codes.yml define your standard API errors, including status_code, title, details and an internal code you can then use to provide further info about the error on your API documentation.

Here's a basic example:

api:
  invalid_resource:
    code: '1'
    status: '400'
    title: 'Bad Request'

not_found:
    code: '2'
    status: '404'
    title: 'Not Found'
    details: 'Resource not found.'

On config/initializers/api_errors.rb load that YAML file into a constant.

API_ERRORS = YAML.load_file(Rails.root.join('doc','error-codes.yml'))['api']

On app/controllers/concerns/error_handling.rb define a reusable method to render your API errors in JSON format:

module ErrorHandling
  def respond_with_error(error, invalid_resource = nil)
    error = API_ERRORS[error]
    error['details'] = invalid_resource.errors.full_messages if invalid_resource
    render json: error, status: error['status']
  end
end

On your API base controller include the concern so it's available on all the controllers which inherit from it:

include ErrorHandling

You will then be able to use your method on any of those controllers:

respond_with_error('not_found') # For standard API errors
respond_with_error('invalid_resource', @user) # For invalid resources

For example on your users controller you may have the following:

def create
  if @user.save(your_api_params)
    # Do whatever your API needs to do
  else
    respond_with_error('invalid_resource', @user)
  end
end

The errors your API will output will look like this:

# For invalid resources
{
  "code": "1",
  "status": "400",
  "title": "Bad Request",
  "details": [
    "Email format is incorrect"
  ]
}

# For standard API errors
{
  "code": "2",
  "status": "404",
  "title": "Not Found",
  "details": "Route not found."
}

As your API grows, you'll be able to easily add new error codes on your YAML file and use them with this method avoiding duplication and making your error codes consistent across your API.

Comments