Make your Grape+Rails app JSON API compliant for cheap


Author:  | February 23

The 1.0 release of the JSON API specification landed last year at the tail end of May, with big names like Steve Klabnik and Yehuda Katz, Vermont’s own Tyler Kellen, and Dan Gebhardt attached. The goals of the standard are, roughly:

  1. Specify conventions for JSON requests and responses
  2. Future-proof compliant APIs
  3. Sidestep trivial details

Convention Over Configuration

This concept has been a driving factor in a number of “batteries included” frameworks and libraries, especially in the Ruby and Rails communities. By adopting an accepted convention during construction, a lot of the knock-on effects of extending the platform are already handled (or at least the pieces are in place). Ideally, these conventions don’t railroad developers, they just move the common or irrelevant details to the background.

In the case of JSON API, this means less developer time spent worrying about how an endpoint should respond, and more time building.

Future-proofing

Developers are invariably familiar with the pain of an API breakage. That fancy library you’ve been relying on doesn’t expose a method you need, anymore, or the default semantics have changed and the old behavior is now tucked away in a legacy mode.

The JSON API spec tries to avoid this with a philosophy of “never remove, only add”. After 1.0, only additions can be made to the spec. Similarly, some developers working with the new standard have adopted this approach in their API designs.

Painting the bikeshed

It is a truism that people (and perhaps developers, especially) will invest the greatest amount of time in the most trivial details. Thousands upon thousands of words have been written on JSON API design, from what responses should look like to the proper behavior of endpoints to whether sets are more appropriate than arrays and what HTTP response code is correct for a missing record. The spec covers all of these and then some, so developers building to the spec can worry about features instead of reinventing the same wheel.

Now, time for Actual Work™

So, you’ve heard all about Grape and it sounds like just the ticket. We’re going to presume Sequel models just for experimentation’s sake, but 90% of this is applicable to ActiveRecord models, as well. We’re going to thread our Grape API through jsonapi-resources, a library from the aforementioned Dan Gebhardt’s company Cerebris which implements the resource model expected by the spec.

Suppose we’ve got some code like this:

# Gemfile
gem 'pg'
gem 'sequel-rails'
gem 'sequel_secure_password'
gem 'grape'
gem 'grape-jsonapi-resources'
gem 'jsonapi-resources'

# app/models/user.rb
class User < Sequel::Model
  plugin :secure_password

  def validate
    super
    validates_presence [:email, :password_digest, :first_name, :last_name]
  end
end

# app/controllers/api/base.rb
module API
  class Base < Grape::API
    mount API::V1::Base
  end
end

# app/controllers/api/v1/base.rb
module API
  module V1
    class Base < Grape::API
      mount API::V1::Users
    end
  end
end

This looks a little dense, but it’s pretty straightforward. We’re just nesting small endpoing definitions to namespace and version our API.


# app/controllers/concerns/api/v1/defaults.rb
module API
  module V1
    module Defaults
      extend ActiveSupport::Concern

      included do
        version 'v1'
        format :json
        formatter :json, Grape::Formatter::JSONAPIResources
        content_type :json, 'application/vnd.api+json'

        rescue_from Sequel::NoMatchingRow do |exception|
          params = env['api.endpoint'].params
          record_not_found = JSONAPI::Exceptions::RecordNotFound.new(params[:id])
          not_found = JSONAPI::ErrorsOperationResult.new(record_not_found.errors[0].code, record_not_found.errors)
          error! not_found, 404
        end
      end
    end
  end
end

There’s a little bit of ugliness here which bears some explaining. The first few lines of included are just setting up response formatting. The standard dictates the Content-Type, and the formatting will be explained later. The ugliness comes out of how Sequel handles nonexistent records. Rather than raising an exception, it simply returns nil, which is fine (though not very informative) in most cases but absolutely stomps on what we’re doing here. In order to match the standard, we need to 404 when no result is found. Using the normal Model[id] syntax won’t work here, because when we get nil back and try to render it, the whole thing blows up. NG. To deal with this, in the code below we use with_pk!which raises the Sequel::NoMatchingRow exception. After grabbing the params from the endpoint, we wrap it up in the appropriate trappings for JSON API and 404.

And from here we just write the usual endpoint code. Here we’ve got a couple of GETs and a POST.

# app/controllers/api/v1/users.rb
module API
  module V1
    class Users < Grape::API

      include API::V1::Defaults

      resource :users do
        desc 'Return a list of users'
        get do
          users = User.all
          render users
        end

        desc 'Return a user'
        params do
          requires :id, type: Integer, desc: 'User ID'
        end
        route_param :id do
          get do
            user = User.with_pk!(params[:id])
            render user
          end
        end

        desc 'Create a user'
        params do
          requires :email, type: String, desc: 'New user email'
          requires :first_name, type: String, desc: 'New user first name'
          requires :last_name, type: String, desc: 'New user last name'
          requires :password, type: String, desc: 'New user password'
          optional :password_confirmation, type: String, desc: 'New user password confirmation'
        end
        post do
          User.new({
                       email: params[:email],
                       first_name: params[:first_name],
                       last_name: params[:last_name],
                       password: params[:password],
                       password_confirmation: params[:password_confirmation]
                   }).save
        end
      end
    end
  end
end

After mounting the API in routes.rb, the result should be an endpoint at /api/v1/users/:id with properly formatted responses and all.

Want to learn more about how Hark can help?