Chapter 30

Alexandria and The REST Constraints

In this chapter, we are going to go back to Alexandria and analyze why the API we built is not RESTful. We will also discuss why I decided to show you how to build a non-RESTful API first. Finally, we will update Alexandria with a custom media type and hypermedia. We will also check how we could implement stateless authentication.

30.1. Why Didn’t We Build Alexandria as a RESTful API?

First, let’s answer this question:

Why didn’t I show you how to build RESTful APIs right away?

Well, that’s based on the fact that 99% of the APIs out there are not RESTful and you will probably end up working on one of them. It’s important to understand HTTP APIs and their limitations, since you probably won’t be able to rewrite everything from scratch. You might not even want to if your scenario doesn’t benefit from the REST constraints.

30.2. REST Constraints Compliance

Let’s go through each constraint one more time and see if Alexandria supports them.

30.2.1. Client-Server

Not much to say here; any applications running on the Web tends to respect this constraint. That includes Alexandria.

30.2.2. Stateless

Alexandria is clearly not stateless due to the authentication scheme we implemented. We’ve already discussed this facet and why we’ve decided to do that.

Alexandria is alright the way it is now, and I personally believe that going stateless simply gives too much responsibility to the clients.

One way to fix this would be to simply use one of the HTTP authentication scheme, but that’s probably not the best option. If you want to look into stateless authentication, I recommend checking out JSON Web Tokens.

30.2.3. Cache

Any request coming out of Alexandria includes the headers defining if the response can be cached or not. This constraint is respected.

30.2.4. Uniform Interface

The Uniform Interface constraint is actually composed of four sub-constraints.

Identification of Resources

Resources are identified using clean URIs in Alexandria. This constraint is respected.

Manipulation of Resources Through Representations

Resources are manipulated through the representations we send back to the client. Using the identifiers contained in the representations, clients are able to modify or delete representations.

Self-Descriptive Messages

Using HTTP headers correctly, our requests are self-descriptive and clients are able to understand the representation using the embedded metadata.

HATEOAS

This is another constraint we violated. We did so because most APIs today don’t respect it, and you should know how those work. However, this is something we are going to fix in the next section.

30.2.5. Layered System

Nothing much to say here, Alexandria doesn’t prevent the use of proxies or intermediate caches. Constraint respected!

30.2.6. Code-On-Demand (Optional)

Sadly, we don’t have Code-On-Demand. Since it’s an optional constraint, we don’t need to care too much about it.

30.3. Using a Custom Media Type

One of the recommendations given by Fielding for web APIs is that you should not document your API and instead focus on the documentation of your application media type. That’s where you can define the semantics of your application representations instead of explaining which endpoints can be used. This should actually be discovered by using your RESTful API.

A REST API should spend almost all of its descriptive effort in defining the media type(s) used for representing resources and driving application state, or in defining extended relation names and/or hypertext-enabled markup for existing standard media types. Any effort spent describing what methods to use on what URIs of interest should be entirely defined within the scope of the processing rules for a media type (and, in most cases, already defined by existing media types). Failure here implies that out-of-band information is driving interaction instead of hypertext.

—Roy T. Fielding

Let’s implement a custom media type. We are going to call it application/vnd.alexandria.v1+json. This should remind you of the media type used by GitHub.

30.3.1. Checking Out A New Branch

Our application currently works properly. Since we want to add a new feature, let’s create a new branch.

git checkout -b hypermedia

We will be working on this branch until the end of this chapter. Note that we won’t deploy the hypermedia version of Alexandria. It would require fixing all the tests that are going to break by adding our custom media type in the headers of each request.

If you’ve bought one of the bigger packages, you have the updated version of the tests.

30.3.2. Serializing Our Representations

We only want to use the application/vnd.alexandria.v1+json media type from now on. This means we need to change the way we receive data from the client and the way we send data back. If we were writing documentation for this media type, we would explain how the representations are formatted. We would also define what format the client should use to send data to the server.

We are not going to write tests in this chapter. We will simply use curl to ensure that our implementation works as expected.

First, let’s change the representations and only allow the client to request our custom media type. To check which media type the client wants, we need to extract that information from the Accept header. The easiest way to do this, and sort the media types by preference, is to the the rack-accept gem.

Add it to your Gemfile

# Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.5.0'

gem 'rails', '5.2.0'
gem 'pg'
gem 'puma', '~> 3.11'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'carrierwave'
gem 'carrierwave-base64'
gem 'pg_search'
gem 'kaminari'
gem 'bcrypt', '~> 3.1.7'
gem 'pundit'
gem 'money-rails', '1.11.0'
gem 'stripe'
gem 'oj'
gem 'rack-cors', :require => 'rack/cors'
gem 'rack-accept'

# Hidden code

and get it installed.

bundle install

Next, we need to tell Rails that we will be using a new custom media type. We are also going to add a new renderer to make it easier to format our representations.

Open the config/initializers/mime_types.rb file and insert the following code into it.

# config/initializers/mime_types.rb
Mime::Type.register 'application/vnd.alexandria.v1+json', :alexandria_json_v1

# If we need to do some versioning
# Mime::Type.register "application/vnd.alexandria.v2+json", :alexandria_json_v2
# Or use a different base format
# Mime::Type.register "application/vnd.alexandria.v1+xml", :alexandria_json_v1

ActionController::Renderers.add :alexandria_json_v1 do |obj, options|
  # We set the content type here
  self.content_type = Mime::Type.lookup('application/vnd.alexandria.v1+json')
  self.response_body  = obj
end

This code will let us write the following in our controllers:

render alexandria_json_v1: data.to_json

Next, let’s move all our serializers into a new module, called V1, to match our custom media type version.

mkdir app/serializers/alexandria/v1 && \
  mv app/serializers/alexandria/serializer.rb \
   app/serializers/alexandria/v1/serializer.rb &&
  mv app/serializers/alexandria/collection_serializer.rb \
   app/serializers/alexandria/v1/collection_serializer.rb &&
  mv app/serializers/alexandria/entity_serializer.rb \
   app/serializers/alexandria/v1/entity_serializer.rb

In order to use a different media type in the future, like one of the hypermedia formats we will discover in the next chapter, we can simply add a new module in serializers/ since we already encapsulated our custom serializers in the Alexandria module. We could add a new module for each format we want to support: app/serializers/json_api, app/serializers/hal, etc.

Let’s update all our serializers with our new modules.

Serializer

# app/serializers/alexandria/serializer.rb
module Alexandria
  module V1
    class Serializer

      def initialize(data:, params:, actions:, options: {})
        @data = data
        @params = params
        @actions = actions
        @options = options
        @serializer = if @data.is_a?(ActiveRecord::Relation)
          collection_serializer
        else
          entity_serializer
        end
      end

      def to_json
        # We skip caching if the presenter is not configured for it
        return data unless @serializer.cache?

        Rails.cache.fetch("#{@serializer.key}/json", { raw: true }) do
          data
        end
      end

      private

      def data
        { data: @serializer.serialize }.to_json
      end

      def collection_serializer
        CollectionSerializer.new(@data, @params, @actions)
      end

      def entity_serializer
        presenter_klass = "#{@data.class}Presenter".constantize
        presenter = presenter_klass.new(@data, @params, @options)
        EntitySerializer.new(presenter, @actions)
      end

    end
  end
end

Collection Serializer

# app/serializers/alexandria/collection_serializer.rb
module Alexandria
  module V1
    class CollectionSerializer

      def initialize(collection, params, actions)
        @collection = collection
        @params = params
        @actions = actions
      end

      def serialize
        return @collection unless @collection.any?
        return build_data
      end

      def key
        # We hash the key using SHA1 to reduce its size
        @key ||= Digest::SHA1.hexdigest(build_key)
      end

      def cache?
        presenter_class.cached?
      end

      private

      def build_data
        @collection.map do |entity|
          presenter = presenter_class.new(entity, @params)
          EntitySerializer.new(presenter, @actions).serialize
        end
      end

      def presenter_class
        @presenter_class ||= "#{@collection.model}Presenter".constantize
      end

      # Building the key is complex. We need to take into account all
      # the parameters the client can send.
      def build_key
        last = @collection.unscoped.order('updated_at DESC').first
        presenter = presenter_class.new(last, @params)

        updated_at = last.try(:updated_at).try(:to_datetime)
        cache_key = "collection/#{last.class}/#{updated_at}"

        [:sort, :dir, :page, :per, :q].each do |param|
          cache_key << "/#{param}:#{@params[param]}" if @params[param]
        end

        if presenter.validated_fields.present?
          cache_key << "/fields:#{presenter.validated_fields}"
        end

        if presenter.validated_embeds.present?
          cache_key << "/embeds:#{presenter.validated_embeds}"
        end

        cache_key
      end

    end
  end
end

Entity Serializer

# app/serializers/alexandria/entity_serializer.rb
module Alexandria
  module V1
    class EntitySerializer

      def initialize(presenter, actions)
        @presenter = presenter
        @entity = @presenter.object
        @actions = actions
      end

      def serialize
        return @presenter.build(@actions)
      end

      def key
        @key ||= Digest::SHA1.hexdigest(build_key)
      end

      def cache?
        @presenter.class.cached?
      end

      private

      def build_key
        updated_at = @entity.updated_at.to_datetime
        cache_key = "model/#{@entity.class}/#{@entity.id}/#{updated_at}"

        if @presenter.validated_fields.present?
          cache_key << "/fields:#{@presenter.validated_fields}"
        end

        if @presenter.validated_embeds.present?
          cache_key << "/embeds:#{@presenter.validated_embeds}"
        end

        cache_key
      end

    end
  end
end

Our serializers are ready; now, we can change the way serialization works at the controller level. First, we are going to remove the serialize method from the application controller and move it to a new concern.

touch app/controllers/concerns/serialization.rb
# app/controllers/concerns/serialization.rb
module Serialization
  extend ActiveSupport::Concern

  def serialize(data, options = {})
    { json: json(data, options) }
  end

  private

  def json(data, options)
    Alexandria::V1::Serializer.new(data: data,
                                   params: params,
                                   actions: [:fields, :embeds],
                                   options: options).to_json
  end

end

Let’s include this new module in the ApplicationController class. Don’t forget to remove the serialize method from there as well.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include Authentication
  include Authorization
  include Serialization

  rescue_from QueryBuilderError, with: :builder_error
  rescue_from RepresentationBuilderError, with: :builder_error
  rescue_from ActiveRecord::RecordNotFound, with: :resource_not_found

  protected

  def builder_error(error)
    render status: 400, json: {
      error: {
        type: error.class,
        message: error.message,
        invalid_params: error.invalid_params
      }
    }
  end

  def unprocessable_entity!(resource, errors = nil)
    render status: :unprocessable_entity, json: {
      error: {
        message: "Invalid parameters for resource #{resource.class}.",
        invalid_params: errors || resource.errors
      }
    }
  end

  def orchestrate_query(scope, actions = :all)
    QueryOrchestrator.new(scope: scope,
                          params: params,
                          request: request,
                          response: response,
                          actions: actions).run
  end

  # Removed the serialize method

  def resource_not_found
    render(status: 404)
  end
end

Next, we need to extract the media types that the client accepts from the Accept header. We also want to compare those media types with the ones we support to get the best candidate to generate the representation.

That’s the responsibility of the find_acceptable method below. The supported media types are contained in the ACCEPTED_MEDIA_TYPES constant.

Note that we only support one media type:

application/vnd.alexandria.v1+json

If the client uses wildcards, we will also return that media type.

# app/controllers/concerns/serialization.rb
module Serialization
  extend ActiveSupport::Concern

  ACCEPTED_MEDIA_TYPES = {
    '*/*'                                => :alexandria_json_v1,
    'application/*'                      => :alexandria_json_v1,
    'application/vnd.alexandria.v1+json' => :alexandria_json_v1
  }

  def serialize # Hidden Code

  private

  def json # Hidden Code

  def accepted_media_type
    @accepted_media_type ||= find_acceptable
  end

  def find_acceptable
    accept_header = request.headers['HTTP_ACCEPT']
    accept = Rack::Accept::MediaType.new(accept_header).qvalues

    accept.each do |media_type, q|
      return media_type if ACCEPTED_MEDIA_TYPES[media_type]
    end

    nil
  end

end

We can now use a before_action filter to ensure that at least one of the formats accepted by the client is also supported by the server. If that’s not the case, we need to return a 406 Unacceptable Error.

# app/controllers/concerns/serialization.rb
module Serialization
  extend ActiveSupport::Concern

  ACCEPTED_MEDIA_TYPES # Hidden Code

  included do
    before_action :acceptable?
  end

  def serialize # Hidden Code

  def acceptable?
    unacceptable! unless accepted_media_type
  end

  private

  def json # Hidden Code
  def accepted_media_type # Hidden Code
  def find_acceptable # Hidden Code

  def unacceptable!
    accept = request.headers['HTTP_ACCEPT']
    render status: 406, json: {
      message: "No acceptable media type in Accept header: #{accept}",
      acceptable_media_types: ACCEPTED_MEDIA_TYPES.keys
    }.to_json
  end
end

Finally, we need to change the way we render our JSON documents. From now on, we don’t want to use the json renderer anymore; instead, we should use the renderer and the serializer that match the media type accepted by the client.

For that, we have to change the serialize method to use the renderer returned by the ACCEPTED_MEDIA_TYPES constant. To use the correct serializer, we call a method named like the renderer (alexandria_json_v1).

If we had more serialization options, we should extract that logic somewhere else - but since we only support one format, we can get away with it by only adding a method.

# app/controllers/concerns/serialization.rb
module Serialization
  extend ActiveSupport::Concern

  ACCEPTED_MEDIA_TYPES # Hidden Code

  included do
    before_action :acceptable?
  end

  def serialize(data, options = {})
    { renderer => send(renderer, data, options) }
  end

  def acceptable? # Hidden Code

  private

  def alexandria_json_v1(data, options)
    Alexandria::V1::Serializer.new(data: data,
                                   params: params,
                                   actions: [:fields, :embeds],
                                   options: options).to_json
  end

  def renderer
    @render ||= ACCEPTED_MEDIA_TYPES[accepted_media_type]
  end

  def accepted_media_type # Hidden Code
  def find_acceptable # Hidden Code

  def unacceptable!
    accept = request.headers['HTTP_ACCEPT']
    render status: 406, alexandria_json_v1: {
      message: "No acceptable media type in Accept header: #{accept}",
      acceptable_media_types: ACCEPTED_MEDIA_TYPES.keys
    }.to_json
  end

end

Here is the complete Serialization module for reference.

# app/controllers/concerns/serialization.rb
module Serialization
  extend ActiveSupport::Concern

  ACCEPTED_MEDIA_TYPES = {
    '*/*'                                => :alexandria_json_v1,
    'application/*'                      => :alexandria_json_v1,
    'application/vnd.alexandria.v1+json' => :alexandria_json_v1
  }

  included do
    before_action :acceptable?
  end

  def serialize(data, options = {})
    { renderer => send(renderer, data, options) }
  end

  def acceptable?
    unacceptable! unless accepted_media_type
  end

  private

  def alexandria_json_v1(data, options)
    Alexandria::V1::Serializer.new(data: data,
                                   params: params,
                                   actions: [:fields, :embeds],
                                   options: options).to_json  
  end

  # If we had a v2
  # def alexandria_json_v2(data)
  #   Alexandria::V2::Serializer.new(data: data,
  #                                  params: params,
  #                                  actions: [:fields, :embeds],
  #                                  options: options).to_json  
  # end

  def renderer
    @render ||= ACCEPTED_MEDIA_TYPES[accepted_media_type]
  end

  def accepted_media_type
    @accepted_media_type ||= find_acceptable
  end

  def find_acceptable
    accept_header = request.headers['HTTP_ACCEPT']
    accept = Rack::Accept::MediaType.new(accept_header).qvalues

    accept.each do |media_type, q|
      return media_type if ACCEPTED_MEDIA_TYPES[media_type]
    end

    nil
  end

  def unacceptable!
    accept = request.headers['HTTP_ACCEPT']
    render status: 406, alexandria_json_v1: {
      message: "No acceptable media type in Accept header: #{accept}",
      acceptable_media_types: ACCEPTED_MEDIA_TYPES.keys
    }.to_json
  end

end

Let’s write some tests to ensure that the serialization is working as expected. We should write tests for the serializers, but we are just going to skip it (I think you already know how to write tests by now). We still need to make sure our implementation is working, though, so let’s write a few request tests.

touch spec/requests/serialization_spec.rb
# spec/requests/serialization_spec.rb
require 'rails_helper'

RSpec.describe 'Serialization', type: :request do

  let(:api_key) { ApiKey.create }
  let(:headers) do
    {
      'HTTP_AUTHORIZATION' =>
        "Alexandria-Token api_key=#{api_key.id}:#{api_key.key}",
      'HTTP_ACCEPT' => accept
    }
  end
  let(:valid_media_type) { 'application/vnd.alexandria.v1+json; charset=utf-8' }

  context 'with "Accept: application/xml, */*"' do
    let(:accept) { 'application/xml, */*' }

    it 'returns 200 OK' do
      get '/api/books', headers: headers
      expect(response.status).to eq 200
    end

    it 'returns "application/vnd.alexandria.v1+json"' do
      get '/api/books', headers: headers
      expect(response.headers['Content-Type']).to eq valid_media_type
    end

  end

  context 'with "Accept: application/vnd.alexandria.v1+json"' do
    let(:accept) { 'application/vnd.alexandria.v1+json' }

    it 'returns 200 OK' do
      get '/api/books', headers: headers
      expect(response.status).to eq 200
    end

    it 'returns "application/vnd.alexandria.v1+json"' do
      get '/api/books', headers: headers
      expect(response.headers['Content-Type']).to eq valid_media_type
    end

    it 'returns valid JSON' do
      get '/api/books', headers: headers
      expect { JSON.parse(response.body) }.to_not raise_exception
    end
  end

  context 'with "Accept: application/json"' do
    let(:accept) { 'application/json' }

    it 'returns 406 Unacceptable' do
      get '/api/books', headers: headers
      expect(response.status).to eq 406
    end

    it 'returns "application/vnd.alexandria.v1+json"' do
      get '/api/books', headers: headers
      expect(response.headers['Content-Type']).to eq valid_media_type
    end
  end
end

Run the tests.

rspec spec/requests/serialization_spec.rb

Success (GREEN)

...

Serialization
  with "Accept: application/xml, */*"
    returns 200 OK
    returns "application/vnd.alexandria.v1+json"
  with "Accept: application/vnd.alexandria.v1+json"
    returns 200 OK
    returns "application/vnd.alexandria.v1+json"
    returns valid JSON
  with "Accept: application/json"
    returns 406 Unacceptable
    returns "application/vnd.alexandria.v1+json"

Finished in 0.51037 seconds (files took 2.86 seconds to load)
7 examples, 0 failures

Looks good!

We are now letting the client know that the vnd.alexandria.v1+json media type should be used.

30.3.3. Understanding The Data Sent by Clients

The serialization part is done, and we can control versioning at the media type level. Now, we need a way to tell the client when the submitted data for POST and PATCH requests are not in the correct format. In order to support multiple media types, like JSON API (which we will study in the next chapter), it’s a good idea to encapsulate the data parsing into specialized classes. Those classes will be responsible for receiving data in format XYZ and normalize it in a way that our controllers can understand.

Let’s create our first parser! Just like the serializers, parsers are interfaces that allow the client and the server to communicate using various formats. Parsers are for the data received by the server while serializers are for the data sent by the server.

Add a parsers and parsers/alexandria/ folders. We also need a default_parser.rb file.

mkdir -p app/parsers/alexandria/v1 && \
  touch app/parsers/alexandria/v1/default_parser.rb

The default parser is not going to do much. The controllers in Alexandria currently expect data formatted in a specific way, and we are not going to change that. Therefore, this parser will simply replace what Rails does to generate the params object.

We also want a 400 Bad Request to be returned to the client if the request cannot be parsed - this is why we enclosed the parsing in a begin rescue block.

# app/parsers/alexandria/v1/default_parser.rb
module Alexandria
  module V1
    class DefaultParser

      def initialize(request)
        @request = request
      end

      def parse(params)
        body = @request.body.read
        params = params.merge(JSON.parse(body)).to_unsafe_hash unless body.blank?
        ActionController::Parameters.new(params)
      end

    end
  end
end

We are going to create one more module, named RequestParsing, to ensure that data sent by the client can be read by the server.

First, create a new file.

touch app/controllers/concerns/request_parsing.rb

Here is the minimum code for this module. We define which media types are supported for input data with the SUPPORTED_MEDIA_TYPES constant. We are also defining two methods, parser and content_type. The content_type method only extracts the content type specified by the client while parser uses that content type to get the matching parser class from SUPPORTED_MEDIA_TYPES.

# app/controllers/concerns/request_parsing.rb
module RequestParsing
  extend ActiveSupport::Concern

  SUPPORTED_MEDIA_TYPES = {
    'application/vnd.alexandria.v1+json' => Alexandria::V1::DefaultParser
  }

  private

  def parser
    @parser ||= SUPPORTED_MEDIA_TYPES[content_type].new(request, params)
  end

  def content_type
    @content_type ||= request.headers['Content-Type']
  end

end

We also want to ensure that the data is parsed when the client makes a POST or PATCH request since only those two methods come with a body. For that, we use a before_action filter.

We also rescue from the ActionController::BadRequest error (which can be raised by a parser) and return 400 Bad Request to the client.

Finally, we override the params method to use our parser.

# app/controllers/concerns/request_parsing.rb
module RequestParsing
  extend ActiveSupport::Concern

  SUPPORTED_MEDIA_TYPES = {
    'application/vnd.alexandria.v1+json' => Alexandria::V1::DefaultParser
  }

  included do
    rescue_from JSON::ParserError, with: :bad_request
    before_action :supported_media_type?, only: [:create, :update]
  end

  def params
    @params ||= @parse_body ? parser.parse(super) : super
  end

  def supported_media_type?
    unless SUPPORTED_MEDIA_TYPES.keys.include?(content_type)
      unsupported_media_type!
    end

    @parse_body = true
  end

  private

  def parser
    @parser ||= SUPPORTED_MEDIA_TYPES[content_type].new(request)
  end

  def content_type
    @content_type ||= request.headers['Content-Type']
  end

  def bad_request
    render status: :bad_request, alexandria_json_v1: {
      message: 'Malformed Alexandria JSON document.'
    }
  end

  def unsupported_media_type!
    render status: :unsupported_media_type, alexandria_json_v1: {
      message: "Unsupported Media Type in Content-Type header: #{content_type}",
      supported_media_types: SUPPORTED_MEDIA_TYPES.keys
    }
  end

end

Finally, let’s add this new concern to the application controller.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include Authentication
  include Authorization
  include Serialization
  include RequestParsing

  # Hidden Code

  protected

  def builder_error # Hidden Code
  def unprocessable_entity! # Hidden Code
  def orchestrate_query # Hidden Code
  def resource_not_found # Hidden Code
end

Now we need to add some tests.

touch spec/requests/request_parsing_spec.rb
# spec/requests/request_parsing_spec.rb
require 'rails_helper'

RSpec.describe 'Request Parsing', type: :request do

  let(:john) { create(:admin) }
  let(:api_key) { ApiKey.create }
  let(:api_key_str) { "#{api_key.id}:#{api_key.key}" }

  let(:access_token) { AccessToken.create(user: john, api_key: api_key) }
  let(:token_str) { "#{john.id}:#{access_token.generate_token}" }

  let(:headers) do
    {
      'HTTP_AUTHORIZATION' =>
        "Alexandria-Token api_key=#{api_key_str}, access_token=#{token_str}",
      'CONTENT_TYPE' => content_type
    }
  end
  let(:author) { create(:michael_hartl) }
  let(:params) do
     {
       data: attributes_for(:ruby_on_rails_tutorial,
                            author_id: author.id)
     }.to_json
  end

  before { post '/api/books', headers: headers, params: params }

  context 'with "Content-Type: application/vnd.alexandria.v1+json"' do
    let(:content_type) { 'application/vnd.alexandria.v1+json' }

    context 'with valid JSON' do
      it 'returns 201 Created' do
        expect(response.status).to eq 201
      end
    end

    context 'with invalid JSON' do
      let(:params) { '{ "data": { title: "Rails Tutorial"' }

      it 'returns 400 Bad Request' do
        expect(response.status).to eq 400
      end
    end
  end

  context 'with "Content-Type: application/json"' do
    let(:content_type) { 'application/json' }

    it 'returns 415 Unsupported Media Type' do
      expect(response.status).to eq 415
    end

    it 'returns "application/vnd.alexandria.v1+json"' do
      expect(response.headers['Content-Type']).to eq(
        'application/vnd.alexandria.v1+json; charset=utf-8'
      )
    end
  end
end

Run the tests.

rspec spec/requests/request_parsing_spec.rb

Success (GREEN)

...

Request Parsing
  with "Content-Type: application/vnd.alexandria.v1+json"
    with valid JSON
      returns 201 Created
    with invalid JSON
      returns 400 Bad Request
  with "Content-Type: application/json"
    returns 415 Unsupported Media Type
    returns "application/vnd.alexandria.v1+json"

Finished in 0.89863 seconds (files took 2.94 seconds to load)
4 examples, 0 failures

30.4. Going Hypermedia

In the next chapter, we will take a look at a few hypermedia formats but for now, I want to show you how to define your own custom media type. This will be based on the simple format we have in Alexandria, but will also include some hypermedia properties.

JSON, unlike HTML, has no built-in support for hyperlinks. While HTML has links and forms as hypermedia controls, JSON has nothing - which explains the amount of standards being developed on top of it to fill this hole. In the next chapter, we will take a look at the following standards/specifications:

  • HAL (Standard)
  • JSON-LD
  • Hydra
  • Collection+JSON
  • SIREN
  • JSON-API

For now, we are simply going to add hyperlinks to our JSON representation - and our approach will be as simple as possible. In the wild, I would recommend using one of the hypermedia formats we will learn about soon. For Alexandria, however, we will do it from scratch because that’s how we built the whole thing.

Note that we are only going to add the hypermedia properties to books, authors and publishers. This is only to give you an idea of how hypermedia can be added to a web API.

We are going to add a new property to all our representations named links. In there, we will have links pointing to the current entity (with self) and the related resources. For each link, we will have a class, a type, a url and a set of methods.

{
    "id": 1,
    "title": "Ruby Under a Microscope",
    "subtitle": "An Illustrated Guide to Ruby Internals",
    "isbn_10": "1593275617",
    "isbn_13": "9781593275617",
    "description": "Ruby Under a Microscope is a cool book!",
    "released_on": "2013-09-01",
    "publisher_id": 1,
    "author_id": 1,
    "created_at": "2016-06-18T11:18:43.094Z",
    "updated_at": "2016-06-18T11:18:43.094Z",
    "cover": "http://localhost:3000/uploads/book/cover/default/cover.jpg",
    "price_cents": 299,
    "price_currency": "USD",
    "links": {
      "publisher": {
        "class": "publishers",
        "type": "entity",
        "url": "http://localhost:3000/api/publishers/1",
        "methods": ["GET", "PATCH", "DELETE"]
      },
      "author": {
        "class": "authors",
        "type": "entity",
        "url": "http://localhost:3000/api/authors/1",
        "methods": ["GET", "PATCH", "DELETE"]
      },
      "self": {
        "class": "books",
        "type": "entity",
        "url": "http://localhost:3000/api/books/1",
        "methods": ["GET", "PATCH", "DELETE"]
      }
  }
}

We could also use a more compact format as presented below, but we will go with the first one for our simple implementation.

{
    "id": 1,
    "title": "Ruby Under a Microscope",
    "subtitle": "An Illustrated Guide to Ruby Internals",
    "isbn_10": "1593275617",
    "isbn_13": "9781593275617",
    "description": "Ruby Under a Microscope is a cool book!",
    "released_on": "2013-09-01",
    "publisher_id": 1,
    "author_id": 1,
    "created_at": "2016-06-18T11:18:43.094Z",
    "updated_at": "2016-06-18T11:18:43.094Z",
    "cover": "http://localhost:3000/uploads/book/cover/default/cover.jpg",
    "price_cents": 299,
    "price_currency": "USD",
    "links": {
      "self": { "class": "book", "id": "1" },
      "publisher": { "class": "publisher", "id": "1" },
      "author": { "class": "author", "id": "1" }
  },
  "hypermedia": {
    "classes": {
      "publisher": {
        "collection":  {
          "uri": "http://localhost:3000/api/publishers",
          "methods": ["GET", "POST"] },
        "entity":  {
          "uri_template": "http://localhost:3000/api/publishers/{id}",
          "methods": ["GET", "PATCH", "DELETE"] }
      },
      "author": {
        "collection":  {
          "uri": "http://localhost:3000/api/authors",
          "methods": ["GET", "POST"] },
        "entity":  {
          "uri_template": "http://localhost:3000/api/authors/{id}",
          "methods": ["GET", "PATCH", "DELETE"] }
      },
      "book": {
        "collection":  {
          "uri": "http://localhost:3000/api/books",
          "methods": ["GET", "POST"] },
        "entity":  {
          "uri_template": "http://localhost:3000/api/books/{id}",
          "methods": ["GET", "PATCH", "DELETE"] }
      }
    }
  }
}

For the authors, who can have many books, we are going to generate a URL using our filtering system. It would be cleaner to have…

http://localhost:3000/api/authors/1/books

but that would require changing a lot of things. It’s much simpler to use…

http://localhost:3000/api/books?q[author_id_eq]=1

and it has the same result.

{
  "data": {
    "id": 1,
    "given_name": "Pat",
    "family_name": "Shaughnessy",
    "created_at": "2016-06-18T11:18:42.957Z",
    "updated_at": "2016-06-18T11:18:42.957Z",
    "links": {
      "books": {
        "class": "books",
        "type": "collection",
        "uri": "http://localhost:3000/api/books?q[author_id_eq]=1",
        "methods": ["GET", "POST"]
        },
      "self": {
        "class": "authors",
        "type": "entity",
        "uri": "http://localhost:3000/api/authors/1",
        "methods": ["GET","PATCH","DELETE"]
      }
    }
  }
}

Let’s get started! First, we need a class that will generate the hypermedia hash for our collections and entities.

Create a new file in the representation_builders folder.

touch app/representation_builders/hypermedia_template.rb
# app/representation_builders/hypermedia_template.rb
class HypermediaTemplate
  # We need this module to use Rails route helpers
  include Rails.application.routes.url_helpers

  def initialize
  end

  # Used for a list of entities. Will generate a URL like
  # http://localhost:3000/api/books for example.
  # The foreign_key and owner_id are only used to add ownership
  # to collections, i.e. http://localhost:3000/api/books?q[author_id_eq]=1
  def collection(presenter_class, owner_id = nil, foreign_key = nil)
    suffix =  foreign_key ? "?q[#{foreign_key}_eq]=#{owner_id}" : ''
    {
      class:   presenter_class.model_name,
      type:    'collection',
      href:     uri(presenter_class: presenter_class, suffix: suffix),
      methods: presenter_class.collection_methods
    }
  end

  # Used for a list of entities. Will generate a URL like
  # http://localhost:3000/api/books for example.
  def entity(presenter_class, id)
    {
      class:   presenter_class.model_name,
      type:    'entity',
      href:     id ? uri(presenter_class: presenter_class, id: id) : nil,
      methods: id ? presenter_class.entity_methods : []
    }
  end

  private

  def uri(presenter_class:, id: nil, suffix: nil)
    "#{root_url}api/#{presenter_class.model_name}".tap do |uri|
      uri << "/#{id}" if id
      uri << suffix if suffix
    end
  end

end

After that, we need a representation builder that will take care of adding the hypermedia data to the presenter data. This class will be called HypermediaTemplate and we will use it to get the actual formatting done.

Note that we are making hypermedia optional since the code we will implement won’t work for some of our resources (for example, the search).

touch app/representation_builders/hypermedia_builder.rb
# app/representation_builders/hypermedia_builder.rb
class HypermediaBuilder

  def initialize(presenter)
    @presenter = presenter
    @template = HypermediaTemplate.new
  end

  def build
    # Just like caching, we make hypermedia optional
    # It has to be activated in the presenter
    if @presenter.class.hypermedia?
      @presenter.data[:links] ||= {}

      add_hypermedia_for_relationships
      add_hypermedia
    end

    @presenter
  end

  private

  # Add the 'self' link
  def add_hypermedia
    links = @template.entity(@presenter.class, @presenter.object.id)
    @presenter.data[:links][:self] = links
  end

  # Add the related entities links
  def add_hypermedia_for_relationships
    @presenter.class.relations.each do |relationship|
      # Get the presenter class for the related entity
      presenter = "#{relationship.singularize.capitalize}Presenter".constantize

      # Reflect on the associations to get the one matching the related
      # entity name
      association = @presenter.object.class.reflections[relationship]
      foreign_key = association.foreign_key

      # We check if the data on the other side of the relation is a collection
      # and call the appropriate method on HypermediaTemplate instance
      @presenter.data[:links][relationship] = if association.collection?
        @template.collection(presenter, @presenter.object.id, foreign_key)
      else # or not
        @template.entity(presenter, @presenter.object.send(foreign_key))
      end
    end
  end

end

Now we need to update the BasePresenter class. We have used some methods in the hypermedia builder that we expect to be defined in our presenters - read through the comments to see what we added.

# app/presenters/base_presenter.rb
class BasePresenter
  # Hidden Code

  class << self
    # Hidden Code
    def cached # Hidden Code
    def cached? # Hidden Code

    # We need a way to enable hypermedia
    # in specific presenters
    def hypermedia
      @hypermedia = true
    end

    def hypermedia?
      @hypermedia
    end

    # Extract the model name from the presenter name
    def model_name
      @model_name ||= self.to_s.demodulize.underscore
                          .split('_').first.pluralize
    end

    # Default actions available for collection resources
    # Can be overridden in children presenters
    def collection_methods
      ['GET', 'POST']
    end

    # Default actions available for single resources
    # Can be overridden in children presenters
    def entity_methods
      ['GET', 'PATCH', 'DELETE']
    end
  end

  attr_accessor :object, :params, :data

  def initialize # Hidden Code
  def as_json # Hidden Code
  def build # Hidden Code
  def validated_fields # Hidden Code
  def validated_embeds # Hidden Code
  def fields # Hidden Code
  def embeds # Hidden Code

  # Like `fields` and `embeds`, we need a method if we want to use
  # the building system
  def hypermedia
    @hypermedia ||= HypermediaBuilder.new(self).build
  end

  private

  def field_picker # Hidden Code
  def embed_picker # Hidden Code

end

With the hypermedia representation builder, links will be added to each individual entity. We still need to define links at the root level for collections. To do that, we are going to add a new method in the collection serializer that will use the HypermediaTemplate class to build a collection template. Since we want it at the root level, it will actually happen in the Serializer class. Still, we need to have a way for the serializer to check if hypermedia should be added.

# app/serializers/alexandria/collection_serializer.rb
module Alexandria
  module V1
    class CollectionSerializer

      def initialize # Hidden Code
      def serialize # Hidden Code

      def links
        HypermediaTemplate.new.collection(presenter_class)
      end

      def key # Hidden Code
      def cache? # Hidden Code

      private

      def build_data # Hidden Code
      def presenter_class # Hidden Code
      def build_key # Hidden Code
    end
  end
end

To add hypermedia only for collections, we can just check if the current serializer instance respond to the links method. Since only the CollectionSerializer class implements that method, the links property will only be added for collections.

# app/serializers/alexandria/serializer.rb
module Alexandria
  module V1
    class Serializer
      def initialize # Hidden Code
      def to_json # Hidden Code

      private

      def data
        json_hash = { data: @serializer.serialize }
        json_hash[:links] = @serializer.links if @serializer.respond_to?(:links)
        json_hash.to_json
      end

      def collection_serializer # Hidden Code
      def entity_serializer # Hidden Code
    end
  end
end

We now need to tell our serializer to use hypermedia. Add the hypermedia action to the alexandria_json_v1 method in the Serialization module.

# app/controllers/concerns/serialization.rb
module Serialization
  # Hidden Code

  def serialize # Hidden Code
  def acceptable? # Hidden Code

  private

  def alexandria_json_v1(data, options)
    Alexandria::V1::Serializer.new(data: data,
                                   params: params,
                                   actions: [:fields, :embeds, :hypermedia],
                                   options: options).to_json
  end

  def renderer # Hidden Code
  def accepted_media_type # Hidden Code
  def find_acceptable # Hidden Code
  def unacceptable! # Hidden Code
end

Finally, let’s enable hypermedia in the presenters for books, publishers and authors.

# app/presenters/book_presenter.rb
class BookPresenter < BasePresenter
  cached
  hypermedia

  # Hidden Code
end
# app/presenters/book_presenter.rb
class AuthorPresenter < BasePresenter
  cached
  hypermedia

  # Hidden Code
end
# app/presenters/book_presenter.rb
class PublisherPresenter < BasePresenter
  cached
  hypermedia

  # Hidden Code
end

To finish our very simple hypermedia implementation, we need to create an entry point. From that entry point, clients should be able to proceed to other resources simply by following links.

Let’s create a new controller for that.

touch app/controllers/home_controller.rb
# app/controllers/home_controller.rb
class HomeController < ApplicationController
  before_action :skip_authorization

  def index
    render alexandria_json_v1: data.to_json
  end

  private

  def data
    template = HypermediaTemplate.new
    presenters = [BookPresenter, AuthorPresenter, PublisherPresenter]

    array = presenters.each_with_object([]) do |presenter, tmp_array|
      tmp_array << template.collection(presenter)
    end

    { data: array }
  end

end

To route requests to this controller, we need to add some configuration in the routes.rb file. We want the root path (/) and the api path (/api) to point to the index action we just created.

# config/routes.rb
Rails.application.routes.draw do

  scope :api do
    # Hidden Code

    get '/', to: 'home#index'
  end

  root to: 'home#index'
end

Let’s give it a try with curl. We are going to start from the homepage and only follow links available in the representation.

First, start the server.

rails s

Send a request to the homepage.

curl -H "Authorization: Alexandria-Token api_key=1:my_api_key" \
     -H "Accept: application/vnd.alexandria.v1+json" \
     http://localhost:3000

Output

{
  "data":[
    {
      "class":"books",
      "type":"collection",
      "href":"http://localhost:3000/api/books",
      "methods":["GET","POST"]
    },{
      "class":"authors",
      "type":"collection",
      "href":"http://localhost:3000/api/authors",
      "methods":["GET","POST"]
    },{
      "class":"publishers",
      "type":"collection",
      "href":"http://localhost:3000/api/publishers",
      "methods":["GET","POST"]
    }
  ]
}

We got back three resources that we can access. We know that we can send GET and POST requests, but we don’t know how to format the data for the POST ones. Unfortunately, our simple media type doesn’t handle that case.

Let’s get the books by using the href property from the representation above.

curl -H "Authorization: Alexandria-Token api_key=1:my_api_key" \
     -H "Accept: application/vnd.alexandria.v1+json" \
     http://localhost:3000/api/books

Output

{
  "data":[
    {
      "id":1,
      "title":"Ruby Under a Microscope",
      "subtitle":"An Illustrated Guide to Ruby Internals",
      "isbn_10":"1593275617",
      "isbn_13":"9781593275617",
      "description":"Ruby Under a Microscope is a cool book!",
      "released_on":"2013-09-01",
      "publisher_id":1,
      "author_id":1,
      "created_at":"2016-06-18T11:18:43.094Z",
      "updated_at":"2016-06-18T11:18:43.094Z",
      "cover":"http://localhost:3000/uploads/book/cover/default/cover.jpg",
      "price_cents":299,
      "price_currency":"USD",
      "links": {
        "publisher": {
          "class":"publishers",
          "type":"entity",
          "href":"http://localhost:3000/api/publishers/1",
          "methods":["GET","PATCH","DELETE"]
        },
        "author": {
          "class":"authors",
          "type":"entity",
          "href":"http://localhost:3000/api/authors/1",
          "methods":["GET","PATCH","DELETE"]
        },
        "self": {
          "class":"books",
          "type":"entity",
          "href":"http://localhost:3000/api/books/1",
          "methods":["GET","PATCH","DELETE"]
        }
      }
    },
    { ... }
  ]
}

We received a list of books, and for each one of them we can access their resource directly or ask for a related resource (author, book). Let’s get more details about the author of the first book.

curl -H "Authorization: Alexandria-Token api_key=1:my_api_key" \
     -H "Accept: application/vnd.alexandria.v1+json" \
     http://localhost:3000/api/authors/1

Output

{
  "data":{
    "id":1,
    "given_name":"Pat",
    "family_name":"Shaughnessy",
    "created_at":"2016-06-18T11:18:42.957Z",
    "updated_at":"2016-06-18T11:18:42.957Z",
    "links":{
      "books":{
        "class":"books",
        "type":"collection",
        "href":"http://localhost:3000/api/books?q[author_id_eq]=1",
        "methods":["GET","POST"]
      },
      "self":{
        "class":"authors",
        "type":"entity",
        "href":"http://localhost:3000/api/authors/1",
        "methods":["GET","PATCH","DELETE"]
      }
    }
  }
}

Great! Now if we wanted, we could get all the books written by Pat.

Alexandria could now be considered partially hypermedia; clients can head to the home page and navigate to /books, /authors and /publishers, and from each one of those resources, they can access the individual items and their relations.

30.5. Pushing our changes

Let’s push the changes.

git status

Output

On branch hypermedia
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   Gemfile
	modified:   Gemfile.lock
	modified:   app/controllers/application_controller.rb
	modified:   app/presenters/author_presenter.rb
	modified:   app/presenters/base_presenter.rb
	modified:   app/presenters/book_presenter.rb
	modified:   app/presenters/publisher_presenter.rb
	deleted:    app/serializers/alexandria/collection_serializer.rb
	deleted:    app/serializers/alexandria/entity_serializer.rb
	deleted:    app/serializers/alexandria/serializer.rb
	modified:   config/initializers/mime_types.rb
	modified:   config/routes.rb

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	app/controllers/concerns/request_parsing.rb
	app/controllers/concerns/serialization.rb
	app/controllers/home_controller.rb
	app/parsers/
	app/representation_builders/hypermedia_builder.rb
	app/representation_builders/hypermedia_template.rb
	app/serializers/alexandria/v1/
	spec/requests/request_parsing_spec.rb
	spec/requests/serialization_spec.rb

no changes added to commit (use "git add" and/or "git commit -a")

Stage them.

git add .

Commit the changes.

git commit -m "Implement Hypermedia"

Push to GitHub.

git push origin hypermedia

30.6. Wrap Up

The hypermedia format we implemented was custom-made, and a simple one at that. We could have made it much better, but we don’t have to (nor should we) reinvent the wheel. A better solution is to use one of the options presented in the next chapter.

In the real world, we would have to document our media type. If you’d like to know how the documentation for a media type looks like, I recommend checking out the JSON API specification as it’s easy to read and really makes you understand how your API should be documented. Note that we will learn more about this specification in the next chapter anyway.