JsonapiForRails
A Rails 4+ plugin for providing JSONAPI v1.0 compliant APIs from your application with very little coding.
- Installation
- Usage
- Set up one API controller per model
- Configure your API controller routes
- Verify your setup
- Modifying the default API behaviour
- Client authentication
- Access control
- Overriding an API end-point
- Implementation status
- Contributing
- License
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 tofalse
disables link generation, and speeds up your API. The default value istrue
. -
content_negotiation
: Setting this tofalse
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 istrue
.
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:
- Content negotiation is implemented and enabled by default, but can be disabled.
- Link generation is implemented and enabled by default, but can be disabled.
- Inclusion of related resources is currently only implemented for requests that return a single resource, and relationship paths are not supported.
- Sparse fieldsets is currently only implemented for requests that return a single resource.
- Sorting is currently not implemented.
- Pagination is currently not implemented.
- Deleting resources is currently not implemented.
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.