Vermont Website Design & Development

JSON API Easy Mode


Author:  | February 24

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": "[email protected]",
      "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": "[email protected]"
      }
    }
  ]
}

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?