jsonapi_for_rails

A Rails plugin for providing JSON API compliant APIs with very little coding. http://jsonapi.org/format/ http://rubyonrails.org/

19
3
Ruby

JsonapiForRails

A Rails 4+ plugin for providing JSONAPI v1.0 compliant APIs from your application with very little coding.

Installation

$ # Optional security step (do this once)
$ gem cert --add <(curl -Ls https://raw.githubusercontent.com/doga/jsonapi_for_rails/master/certs/doga.pem)
$
$ # Go to the root directory of your existing Rails application
$ cd path/to/railsapp
$
$ # Update the gem file
$ echo "gem 'jsonapi_for_rails'" >> Gemfile
$
$ # Install
$ # (Optional security paramater: --trust-policy MediumSecurity)
$ bundle install --trust-policy MediumSecurity
$
$ # Check the used version
$ bin/rails console
irb(main):001:0> JsonapiForRails::VERSION
=> "0.2.1"
irb(main):002:0> exit
$

Usage

1. Set up one API controller per model

Generate a controller for each model that will be accessible from your API. Controller names need to be the plural version of your model names.

$ cd path/to/railsapp
$
$ # Generate your models
$ bin/rails generate model article
$ bin/rails generate model author
$
$ # Generate your API controllers
$ bin/rails generate controller articles
$ bin/rails generate controller authors

Then enable JSONAPI in a parent class of your API controllers.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base # or ActionController::API

  # Enable JSONAPI
  acts_as_jsonapi_resources(
    # links:               false,
    # content_negotiation: false
  )

  # ...
end

acts_as_jsonapi_resources accepts the following keyword arguments:

  • links: Setting this to false disables
    link generation,
    and speeds up your API. The default value is true.
  • content_negotiation: Setting this to false disables
    content negotiation. Again, this helps speed up your API, but at the expense of making your API non-JSONAPI-compliant, if only just). The default value is true.

If only some of your controllers are JSONAPI controllers, then create a parent controller for only those controllers, and enable JSONAPI inside that controller rather than ApplicationController.

$ cat > app/controllers/jsonapi_resources_controller.rb

class JsonapiResourcesController < ApplicationController
  acts_as_jsonapi_resources

  # ...
end
# app/controllers/articles_controller.rb

# Change the API controller's parent class
class ArticlesController < JsonapiResourcesController
  # ...
end

# Do the same with AuthorsController

2. Configure your API controller routes

Update your application routes as follows:

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

  scope '/api/v1' do # Optional scoping

    [ # List your API controllers here
      :articles, :authors
    ].each do |resources_name|
      resources resources_name do
        controller resources_name do
          get     'relationships/:relationship', action: "relationship_show"
          patch   'relationships/:relationship', action: "relationship_update"
          post    'relationships/:relationship', action: "relationship_add"
          delete  'relationships/:relationship', action: "relationship_remove"
        end
      end
    end

  end

  # ...
end

3. Verify your setup

After populating your database and launching the built-in Rails server with the bin/rails server shell command, you can issue some HTTP requests to your API and verify the correctness of the responses.

$ # Get the list of articles
$ # (the returned HTTP response body is short and terse, but is prettified here for legibility)
$ curl 'http://localhost:3000/api/v1/articles'
{
  "data": [
    {
      "type": "articles",
      "id": "618037523"
    },
    {
      "type": "articles",
      "id": "994552601"
    }
  ],
  "links": {
    "self": "/api/v1/articles"
  }
}
$ # Get an article
$ curl 'http://localhost:3000/api/v1/articles/618037523'
{
  "data": {
    "type": "articles",
    "id": "618037523",
    "attributes": {
      "title": "UK bank pay and bonuses in the spotlight as results season starts",
      "content": "The pay deals handed to the bosses of Britain’s biggest banks ...",
      "created_at": "2016-03-02 14:33:49 UTC",
      "updated_at": "2016-03-02 14:33:49 UTC"
    },
    "relationships": {
      "author": {
        "data": {
          "type": "authors",
          "id": "1023487079"
        }
      }
    },
    "links": {
      "self": "/api/v1/articles/618037523"
    }
  }
}
$ # Get only the title and author of an article, include the author's name
$ curl 'http://localhost:3000/api/v1/articles/618037523?filter%5Barticles%5D=title,author;include=author;filter%5Bauthors%5D=name'
{
  "data": {
    "type": "articles",
    "id": "618037523",
    "attributes": {
      "title": "UK bank pay and bonuses in the spotlight as results season starts"
    },
    "relationships": {
      "author": {
        "data": {
          "type": "authors",
          "id": "1023487079"
        }
      }
    },
    "links": {
      "self": "/api/v1/articles/618037523"
    }
  },
  "include": [
    {
      "data": {
        "type": "authors",
        "id": "1023487079",
        "attributes": {
          "name": "Jill T..."
        },
        "relationships": {
        },
        "links": {
          "self": "/api/v1/authors/1023487079"
        }
      }
    }
  ]
}
$

Modifying the default API behaviour

By default, all API end-points are accessible to all clients, and all end-points behave the same way for all clients. In a real-world setting, you may want to restrict access to an end-point and/or change the behaviour of an end-point depending on the client.

Client authentication

Clients can be authenticated with a before_action method in your API controller. Inside controllers, instance variable names starting with the @jsonapi_ prefix and method names starting with the jsonapi_ prefix are reserved by jsonapi_for_rails, so try to avoid those.

# app/controllers/jsonapi_resources_controller.rb
class JsonapiResourcesController < ApplicationController
  acts_as_jsonapi_resources

  before_action :authenticate

private
  def authenticate
    # ...
  end
end

Access control

Access control for authenticated and unauthenticated clients can be implemented in before_action methods in your API controllers.

# app/controllers/jsonapi_resources_controller.rb
class JsonapiResourcesController < ApplicationController
  acts_as_jsonapi_resources

  before_action :permit_read, only: [
    :index,
    :show,
    :relationship_show
  ]

  before_action :permit_write, only: [
    :create, 
    :update, 
    :destroy,
    :relationship_update,
    :relationship_add,
    :relationship_remove
  ]

private
  def permit_read
    # ...
  end

  def permit_write
    # ...
  end
end

Overriding an API end-point

The bin/rails routes shell command shows you the end-points that jsonapi_for_rails defines. In order to change the behaviour of an action, you can define an action with the same name inside an API controller. jsonapi_for_rails provides utility methods and instance variables that can help you.

# app/controllers/articles_controller.rb
class ArticlesController < JsonapiResourcesController 

  def index
    # These model-related utility methods are available inside all action methods.
    jsonapi_model_class # =>  Article
    jsonapi_model_type  # => :articles

    # @jsonapi_links indicates whether links should be included in response documents.
    # It is available inside all action methods.
    @jsonapi_links      # => true

    # ...
  end

  def show
    # @jsonapi_record contains the current database record.
    # It is available inside all action methods (including all relationship
    # methods) except :index and :create.
    @jsonapi_record.to_jsonapi_hash        # => {data:   {...}}
    @jsonapi_record.to_jsonapi_errors_hash # => {errors: [...]}

    # ...
  end

  def relationship_show
    # @jsonapi_relationship is available in all relationship action methods.
    # @jsonapi_relationship[:definition] describes the current relationship.
    @jsonapi_relationship # => {:definition=>{:name=>:author, :type=>:to_one, :receiver=>{:type=>:authors, :class=>Author}}}

    # ...
  end

  def relationship_update
    # @jsonapi_relationship[:params] contains the parsed request body.
    # It is available for all relationship action methods except relationship_show.
    # @jsonapi_relationship[:params][:data] behaves like a Hash for relationships
    # of type :to_one, and as an Array for relationships of type :to_many.
    @jsonapi_relationship # => {:definition=>{...}, :params=>{"data"=>{"type"=>"authors", "id"=>"234455384"}}}

    # ...
  end

end

Implementation status

The internal architecture is sound. Test coverage is currently being bulked up using Rails 5 beta 2 (but the plugin should be compatible with Rails 4+). And missing features are being added. The intention is to release a 1.0 version around mid-2016.

Feature support roundup:

Contributing

Feel free to share your experience using this software. If you find a bug in this project, have trouble following the documentation or have a question about the project – create an issue.

License

The gem is available as open source under the terms of the MIT License.