JSON API Easy Mode
Author: Aimee Albro - Account Director
My last post was about making Grape play nice with the JSON API spec, but I also used a library in that post which nearly obviates the need for Grape altogether. If you thought Grape was opinionated, you haven’t seen anything until you’ve seen jsonapi-resources
.
There’s a reason for that, of course. The company that maintains JR (as they call it), Cerebris, is Dan Gebhardt’s company. The same Dan Gebhardt who helped write the specification in the first place.
The setup will be a little different here. JR only supports ActiveRecord, as far as I can tell (unless you come at it sideways through Grape, it seems), so we’ll just use that.
We’ll start with our routes, and I’ll assume we’re dealing with a user endpoint like in the last post. I’ll also assume a totally default scaffold like you’d get with rails generate scaffold
.
Rails.application.routes.draw do
resources :users
namespace :api do
namespace :v1 do
jsonapi_resources :users
end
end
Once again, we’re namespacing everything. We’ll end up with an endpoint at /api/v1/users
, just like in the last example.
First, we need to write a controller. Luckily, the defaults are really smart, so for us this just looks like this:
# app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < JSONAPI::ResourceController
# index, show, create, update, destroy methods can be overridden, but we won't, for now
end
That’s really it. In the next release, you’ll actually just be able to generate this with rails generate jsonapi:controller api::v1::user
.
So, suppose we have an analagous model to the one in the last post:
class User < ActiveRecord::Base
has_secure_password
validates :first_name, :last_name, :email, :password, presence: true
end
This is already accessible through our usual frontend controller, but we want our JR controller to know about it, too. This is where resources come in. Writing a resource is very straightforward if you follow conventions, and only slightly less so if you need something more complex.
A conventional resource looks like this:
class Api::V1::UserResource < JSONAPI::Resource
attributes :first_name, :last_name, :email, :password
def fetchable_fields
super - [:password]
end
end
The resource describes all the attributes available for API requests. By overriding fetchable_fields
, we can remove sensitive fields like passwords from GETs, while still requiring them in POSTs.
We end up with routes like this:
api_v1_users GET /api/v1/users(.:format) api/v1/users#index
POST /api/v1/users(.:format) api/v1/users#create
api_v1_user GET /api/v1/users/:id(.:format) api/v1/users#show
PATCH /api/v1/users/:id(.:format) api/v1/users#update
PUT /api/v1/users/:id(.:format) api/v1/users#update
DELETE /api/v1/users/:id(.:format) api/v1/users#destroy
Now we can create a user by POSTing the following to /api/v1/users
:
{
"data": {
"type": "users",
"attributes": {
"email": "foo@bar.baz",
"first-name": "Foo",
"last-name": "Bar",
"password": "changethis"
}
}
}
And by GETting on the same endpoint, we get back nicely formatted spec-compliant JSON like this:
{
"data": [
{
"id": "1",
"type": "users",
"links": {
"self": "http://localhost:3000/api/v1/users/1"
},
"attributes": {
"first-name": "Foo",
"last-name": "Bar",
"email": "foo@bar.baz"
}
}
]
}
Near zero boilerplate, standards compliant, and nice features out of the box. The library is flexible enough for embedding in an existing application, mounting as an engine, and just about any other configuration you could imagine.
On top of that, error reporting complies with the standard and is informative for API consumers.
Suppose our consumer forgot the last-name
field in their POST. An 422 (Unprocessable Entity) error response like this comes back:
{
"errors": [
{
"title": "can't be blank",
"detail": "last-name - can't be blank",
"id": null,
"href": null,
"code": "100",
"source": {
"pointer": "/data/attributes/last-name"
},
"links": null,
"status": "422",
"meta": null
}
]
}
This tells the consumer what went wrong, the source of the error, and gives a human readable explanation. Similarly, requesting a nonexistent record gives the 404 required by the standard like this for GET /api/v1/users/4
:
{
"errors": [
{
"title": "Record not found",
"detail": "The record identified by 4 could not be found.",
"id": null,
"href": null,
"code": "404",
"source": null,
"links": null,
"status": "404",
"meta": null
}
]
}
At very least, I’d say the spec has good answers for a lot of the annoying implementation details that go into building an API into your application, and JR does a great job doing the heavy lifting when it comes to meeting the requirements of the spec.
Want to learn more about how Hark can help?