Make Your Grape+Rails App JSON API Compliant for Cheap
Author: Aimee Albro - Account Director
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 Gebhardtattached. The goals of the standard are, roughly:
- Specify conventions for JSON requests and responses
- Future-proof compliant APIs
- 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.
Futureproofing
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?