The JSON API spec for buliding HTTP APIs using json is a great initiative in stopping bike-shedding in API design. It has its trade-offs and cavaets but overall, I like it.

The jsonapi-resources gem supports the creation of APIs that follow the JSON API spec. This library is built to work with Rails, exposing your ActiveRecord models as JSON API resources.

The tutorial for that integration is good if the model you’re exposing is backed by ActiveRecord. Using a non-ActiveRecord can take a little bit more work.

Here’s an example. (assume you have jsonapi-resources in your Gemfile)

Assume we have a HealthStatus class that is responsible for knowing the status of our API (i.e. whether the API is “up” or “down”):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# app/models/health_status.rb

class HealthStatus
  def self.current_status
    # Figures out the status of the internal and
    # 3rd party services (implementation not included here)

    HealthStatus.new(services: true, third_party_services: false)
  end

  def initialize(services: false, third_party_services: false)
    @id = SecureRandom.uuid
    @services = services
    @third_party_services = third_party_services
  end

  def services_up?
    @services
  end

  def third_party_services_up?
    @third_party_services
  end
end

We want the new API to check health statuses to have the url /health-statuses, so as usual in Rails, we’ll add a route:

1
2
3
4
5
6
7
# config/routes.rb

Rails.application.routes.draw do
  # This provides more than the GET /health-statuses,
  # but we're doing this for simplicity in the example
  jsonapi_resources :health_statuses
end

The controller is straight-forward, assuming we take the default actions from jsonapi-resources:

1
2
3
4
5
6
# app/controllers/health_statuses_controller.rb

class HealthStatusesController < JSONAPI::ResourceController
  # JSONAPI::ResourceController provides #show, #create, #destroy,
  # #update implementations
end

We now need to define the Resource that is the object representation of the resource we’re going to expose in the API. The Resource is marshalled and unmarhsalled into JSON.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# app/resources/health_status_resource.rb

class HealthStatusResource < JSONAPI::Resource
  attributes :services, :third_party_services

  # Override the method used to GET /health-statuses
  # The implementation from JSONAPI::Resource relies
  # on our @model being an ActiveRecord. Since the
  # HealthStatus isn't an ActiveRecord, we provide
  # our own implementation
  def self.find_by_key(_key, options = {})
    context = options[:context]
    model = HealthStatus.current_status
    new(model, context)
  end

  # The default implementation assumes that our @model's
  # id is an integer. In our case, each HealthStatus
  # provides an id that is a UUID. We can't use the default
  # implementation that checks for integer ids.
  def self.verify_key(key, _context = nil)
    key && String(key)
  end

  def services
    status_message(@model.services_up?)
  end

  def third_party_services
    status_message(@model.third_party_services_up?)
  end

  private

  def status_message(status)
    message = 'down'
    message = 'up' if status
    message
  end
end

The key change here is to override ::find_by_key to provide our own way of finding the instance of the model we want for a given API request.

Sending a HTTP GET to /health-statuses will return:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "data": {
    "id": "34b7f196-38c1-4eba-96e1-609ff92a671d",
    "type": "health-statuses",
    "attributes": {
      "services": "up",
      "third-party-services": "down",
    },
    "links": {
      "self": "http://localhost:3000/health-statuses/34b7f196-38c1-4eba-96e1-609ff92a671d"
    }
  }
}

You’ll notice that jsonapi-resources provides the self link for the resource automatically. I don’t actually support finding health-statuses by id and haven’t found a way to remove the auto-linking yet.