This app demonstrates the use of the JsonApiServer gem https://github.com/ed-mare/json_api_server. It is a Rails 5.1.x app which is known to work with Ruby 2.4.1. It uses sqlite and is configured for file-based caching.
Run this app in development environment only.
Checkout this repository, cd
to the root of the app and run:
# install gems
bundle install
# create database and seed it
rake db:migrate
rake db:seed
# start server
bundle exec rails s -p 3000
If using docker:
# Docker image known to work on Ubuntu 14.04 and 16.04 with docker 1.3.1+
# build the web image
docker-compose build
# start bash session and run tasks to build/seed database
docker-compose run --rm web /bin/bash
rake db:migrate
rake db:seed
# start the server
docker-compose up
Cleanup file cache:
# cd to root of app
rm -rf tmp/file_store_cache/*
- Filters can only be applied to the parent resource, however, model queries and custom builders can perform joins, etc. to query associations.
- Sort can only be applied to the parent resource.
- Nested
included
sections are not bubbled up to the parentincluded
section.
Error handling module is included in controllers/applicaiton_controller.rb
.
'config/locales/en.yml' customizes errors messages.
application/vnd.api+json is configured in config/initializers/mime_types.rb
Configurations are in config/initializers/json_api_server.rb
config.active_support.escape_html_entities_in_json = false
...is added to application.rb so pagination URLs aren't escaped. This is an OJ/Rails compatibility configuration: https://github.com/ohler55/oj/blob/master/pages/Rails.md
Serializers are in app/serializers
folder.
Includes, filter, sort, and pagination configurations are in the controllers.
The publishers controller demonstrates eager loading related resources (includes).
# relationships 2 levels deep - publisher books and book author
http://localhost:3000/api/v1/publishers?include=publisher.books,book.author&fields[books]=title&fields[authors]=first_name,last_name
It performs queries like...
SELECT "publishers".* FROM "publishers" ORDER BY "publishers"."id" DESC LIMIT ? OFFSET ? [["LIMIT", 10], ["OFFSET", 0]]
SELECT "books".* FROM "books" WHERE "books"."publisher_id" IN (5, 4, 3, 2, 1)
SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (5, 4, 3, 2, 1)
Each publisher looks something like...
{
"type": "publishers",
"id": 2,
"attributes": {
"name": "George Allen & Unwin",
"country": "Australia",
"created": "2017-09-19T02:28:24Z",
"updated": "2017-09-19T02:28:24Z"
},
"relationships": {
"books": [
{
"data": {
"type": "books",
"id": 1,
"attributes": {
"title": "The Lord of the Rings"
},
"relationships": {
"author": {
"data": {
"type": "authors",
"id": 2,
"attributes": {
"first_name": "J.",
"last_name": "Tolkien"
},
"relationships": {}
}
}
}
}
},
{
"data": {
"type": "books",
"id": 2,
"attributes": {
"title": "The Hobbit"
},
"relationships": {
"author": {
"data": {
"type": "authors",
"id": 2,
"attributes": {
"first_name": "J.",
"last_name": "Tolkien"
},
"relationships": {}
}
}
}
}
}
]
}
}
The books controller demonstrates low level caching of related resources.
http://localhost:3000/api/v1/books?include=book.checkouts,book.comments,comment.patron,checkout.patron&fields[books]=title&fields[comments]=text&fields[patrons]=first_name,last_name&fields[checkouts]=checkout_date
After caching for the first time, it performs queries like...
SELECT COUNT(*) FROM "books"
SELECT "books".* FROM "books" ORDER BY "books"."id" DESC LIMIT ? OFFSET ? [["LIMIT", 10], ["OFFSET", 0]]
Each book should look something like...
{
"type": "books",
"id": 17,
"attributes": {
"title": "Through the Looking-Glass"
},
"relationships": {
"comments": [
{
"data": {
"type": "comments",
"id": 18,
"attributes": {
"text": "One good thing about being young is that you are not experienced enough to know you\n cannot possibly do the things you are doing."
},
"relationships": {
"patron": {
"data": {
"type": "patrons",
"id": 8,
"attributes": {
"first_name": "Gayla",
"last_name": "Gearheart"
},
"relationships": {}
}
}
}
}
},
{
"data": {
"type": "comments",
"id": 15,
"attributes": {
"text": "Stretch your vision. See what can be, not just what is. Practice adding value to things, to people and to yourself."
},
"relationships": {
"patron": {
"data": {
"type": "patrons",
"id": 18,
"attributes": {
"first_name": "Un",
"last_name": "Ursery"
},
"relationships": {}
}
}
}
}
},
{
"data": {
"type": "comments",
"id": 6,
"attributes": {
"text": "The worst speak something good; if all want sense, God takes a text, and preacheth Patience."
},
"relationships": {
"patron": {
"data": {
"type": "patrons",
"id": 17,
"attributes": {
"first_name": "Vennie",
"last_name": "Valenzuela"
},
"relationships": {}
}
}
}
}
},
{
"data": {
"type": "comments",
"id": 5,
"attributes": {
"text": "Those who believe that they are exclusively in the right are generally those who achieve something."
},
"relationships": {
"patron": {
"data": {
"type": "patrons",
"id": 20,
"attributes": {
"first_name": "Fredrick",
"last_name": "Filler"
},
"relationships": {}
}
}
}
}
},
{
"data": {
"type": "comments",
"id": 1,
"attributes": {
"text": "Although the whole of this life were said to be nothing but a dream and the physical\n ruby muworld nothing but a phantasm, I should call this dream or phantasm real enough,\n if, using reason well, we were never deceived by it."
},
"relationships": {
"patron": {
"data": {
"type": "patrons",
"id": 20,
"attributes": {
"first_name": "Fredrick",
"last_name": "Filler"
},
"relationships": {}
}
}
}
}
}
],
"checkouts": {
"data": {
"type": "checkouts",
"id": 8,
"attributes": {
"checkout_date": "2003-11-25"
},
"relationships": {
"patron": {
"data": {
"type": "patrons",
"id": 15,
"attributes": {
"first_name": "Santiago",
"last_name": "Stoner"
},
"relationships": {}
}
}
}
}
}
}
},
The custom model query does a wildcard query against author first_name OR last_name.
http://localhost:3000/api/v1/books?filter[author]=christie
http://localhost:3000/api/v1/books?include=book.publisher,book.comments&filter[published]=>2000-01-01&filter[published1]=<2016-01-01
http://localhost:3000/api/v1/books?filter[author_id]=1,2,3
Should return something like...
{
"jsonapi": {
"version": "1.0"
},
"links": {
"first": "http://localhost:3000/api/v1/books?fields%5Bbooks%5D=title%2Cauthor_id&filter%5Bauthor_id%5D=1%2C2%2C3&page%5Blimit%5D=20&page%5Bnumber%5D=1&sort=author_id",
"last": "http://localhost:3000/api/v1/books?fields%5Bbooks%5D=title%2Cauthor_id&filter%5Bauthor_id%5D=1%2C2%2C3&page%5Blimit%5D=20&page%5Bnumber%5D=1&sort=author_id",
"self": "http://localhost:3000/api/v1/books?fields%5Bbooks%5D=title%2Cauthor_id&filter%5Bauthor_id%5D=1%2C2%2C3&page%5Blimit%5D=20&page%5Bnumber%5D=1&sort=author_id",
"next": null,
"prev": null
},
"data": [
{
"type": "books",
"id": 3,
"attributes": {
"title": "Harry Potter and the Philosopher's Stone",
"author_id": 1
}
},
{
"type": "books",
"id": 4,
"attributes": {
"title": "Harry Potter and the Chamber of Secrets",
"author_id": 1
}
},
{
"type": "books",
"id": 5,
"attributes": {
"title": "Harry Potter and the Prisoner of Azkaban",
"author_id": 1
}
},
{
"type": "books",
"id": 6,
"attributes": {
"title": "Harry Potter and the Goblet of Fire",
"author_id": 1
}
},
{
"type": "books",
"id": 7,
"attributes": {
"title": "Harry Potter and the Order of the Phoenix",
"author_id": 1
}
},
{
"type": "books",
"id": 8,
"attributes": {
"title": "Harry Potter and the Half-Blood Prince",
"author_id": 1
}
},
{
"type": "books",
"id": 9,
"attributes": {
"title": "Harry Potter and the Deathly Hallows",
"author_id": 1
}
},
{
"type": "books",
"id": 1,
"attributes": {
"title": "The Lord of the Rings",
"author_id": 2
}
},
{
"type": "books",
"id": 2,
"attributes": {
"title": "The Hobbit",
"author_id": 2
}
},
{
"type": "books",
"id": 10,
"attributes": {
"title": "Murder on the Orient Express",
"author_id": 3
}
},
{
"type": "books",
"id": 11,
"attributes": {
"title": "The Murder of Roger Ackroyd",
"author_id": 3
}
},
{
"type": "books",
"id": 12,
"attributes": {
"title": "The Murder at the Vicarage",
"author_id": 3
}
},
{
"type": "books",
"id": 13,
"attributes": {
"title": "Partners in Crime",
"author_id": 3
}
},
{
"type": "books",
"id": 14,
"attributes": {
"title": "The A.B.C. Murders",
"author_id": 3
}
},
{
"type": "books",
"id": 15,
"attributes": {
"title": "And Then There Were None",
"author_id": 3
}
}
],
"included": [],
"meta": null
}
http://localhost:3000/api/v1/books?filter[title]=*murder&fields[books]=title
Should return something like...
{
"jsonapi": {
"version": "1.0"
},
"links": {
"first": "http://localhost:3000/api/v1/books?fields%5Bbooks%5D=title&filter%5Btitle%5D=%2Amurder&page%5Blimit%5D=10&page%5Bnumber%5D=1",
"last": "http://localhost:3000/api/v1/books?fields%5Bbooks%5D=title&filter%5Btitle%5D=%2Amurder&page%5Blimit%5D=10&page%5Bnumber%5D=1",
"self": "http://localhost:3000/api/v1/books?fields%5Bbooks%5D=title&filter%5Btitle%5D=%2Amurder&page%5Blimit%5D=10&page%5Bnumber%5D=1",
"next": null,
"prev": null
},
"data": [
{
"type": "books",
"id": 14,
"attributes": {
"title": "The A.B.C. Murders"
}
},
{
"type": "books",
"id": 12,
"attributes": {
"title": "The Murder at the Vicarage"
}
},
{
"type": "books",
"id": 11,
"attributes": {
"title": "The Murder of Roger Ackroyd"
}
},
{
"type": "books",
"id": 10,
"attributes": {
"title": "Murder on the Orient Express"
}
}
],
"included": [],
"meta": null
}
Note: the request returns requested inclusions on success.
cd ./examples_json
curl -vX POST http://localhost:3000/api/v1/books?include=book.author,book.publisher -d @new_book.json --header "Content-Type: application/vnd.api+json"
First time should return:
{
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "http://localhost:3000/api/v1/books/1"
},
"data": {
"type": "books",
"id": 1,
"attributes": {
"title": "An entry added programmatically 1",
"description": "A fake book",
"publication_date": "2015-05-22",
"price": 5.56,
"publisher_id": 1,
"author_id": 1,
"created": "2017-09-19T02:09:47Z",
"updated": "2017-09-19T02:09:47Z"
},
"relationships": {
"author": {
"data": {
"type": "authors",
"id": 1,
"attributes": {
"first_name": "John",
"middle_name": null,
"last_name": "Doe I",
"year_of_birth": 1976,
"created": "2017-09-19T02:09:47Z",
"updated": "2017-09-19T02:09:47Z"
},
"relationships": {}
}
},
"publisher": {
"data": {
"type": "publishers",
"id": 1,
"attributes": {
"name": "FooBar Publishers",
"country": "USA",
"created": "2017-09-19T02:09:47Z",
"updated": "2017-09-19T02:09:47Z"
},
"relationships": {}
}
}
}
},
"included": [],
"meta": null
}
Run it again and it should return (unique index constraint):
{
"jsonapi": {
"version": "1.0"
},
"errors": [
{
"status": 409,
"title": "Conflict",
"detail": "This book already exists."
}
]
}
curl -vX POST http://localhost:3000/api/v1/books?include=book.author,book.publisher -d @new_book_validation_error.json --header "Content-Type: application/vnd.api+json"
Should return something like...
{
"jsonapi": {
"version": "1.0"
},
"errors": [
{
"status": "422",
"source": {
"pointer": "/data/attributes/title"
},
"title": "Invalid Attribute",
"detail": "Title can't be blank"
}
]
}