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”):
# 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:
# 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:
# 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.
# 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:
{
"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.