Ember and Rails 5 with JSON API: A Modern Bridge

Last reviewed in April 2018 with Ember 3.1

The JSON API specification dovetails nicely with the Ember and Rails philosophy of convention over configuration.

Our plan is to put it to work. We'll build a full-blown Ember app hooked to a Rails 5 API using JSON API, step by step.


The app

We will create a simple bookstore.

Are you totally new to Ember? Check out Getting Started with Ember Octane

Authors write books. They are published by either a publishing house or the author himself (self-published).

The bookstore sells books and makes money on discounts offered by publishers. Authors who self-publish offer a fixed discount of 10%.

Setting it up

Rails 5 API-only

Make sure you are running Ruby 2.2.2 or newer: that is what Rails 5 requires. Let's install it.

$ ruby -v
ruby 2.3.5p376 (2017-09-14 revision 59905) [x86_64-darwin16]
$ bundler -v
Bundler version 1.16.1

$ gem install rails
# ...
18 gems installed

Note down the Rails version (5.2.0 in our case). It's possible we have multiple gems if Rails was previously installed in our system. We will create our backend API project ensuring it's Rails 5:

$ rails _5.2.0_ new bookstore-api --api

Ember client app

Let's pick a name and create our shiny new Ember app.

$ ember new bookstore
If you need to update Ember, follow this guide.

Generating models

Now that we have both apps ready, it's time to define and generate our models.

  • Book attributes: title:string, price:decimal, author:belongs-to, publisher:belongs-to(polymorphic)

  • PublishingHouse attributes: name:string, discount:decimal, published-books:has-many

  • Author attributes: name:string, books:has-many, published-books:has-many

  • Publisher is the polymorphic type of PublishingHouse and Author


Using the Rails generator we'll scaffold required files and migrate the database to those changes. Inside the Rails project directory:

$ bin/rails generate scaffold book title 'price:decimal{5,2}' author:references publisher:references{polymorphic}
$ bin/rails g scaffold author name
$ bin/rails g scaffold publishing_house name 'discount:decimal{2,2}'

$ bin/rails db:migrate

All this is fancy but we have no data to interact with…

Sample data can be provided in the db/seeds.rb file.

# db/seeds.rb

pub1 = PublishingHouse.create(name: "ABC Publisher", discount: 40)
pub2 = PublishingHouse.create(name: "Acme Publishing House", discount: 50)
pub3 = PublishingHouse.create(name: "Foobar Corporation", discount: 55)

author1 = Author.create(name: "James Jackson")
author2 = Author.create(name: "Roberta Rock")
author3 = Author.create(name: "Daniel Duck")
author4 = Author.create(name: "Amanda Djidjinski")
author5 = Author.create(name: "Zoe Zack")
author6 = Author.create(name: "Bill Burray")
author7 = Author.create(name: "Charlie Chuck")

book1 = Book.create(title: "The Great Escape", author: author7, publisher: pub1, price: 24.20)
book2 = Book.create(title: "Saving Myself", author: author6, publisher: pub1, price: 14.13)
book3 = Book.create(title: "The Killer Doctors", author: author5, publisher: pub1, price: 15.12)
book4 = Book.create(title: "Marianne", author: author4, publisher: pub1, price: 10.50)
book5 = Book.create(title: "On the Verge of Salvation", author: author4, publisher: pub2, price: 11.76)
book6 = Book.create(title: "Fields of L", author: author3, publisher: pub2, price: 27.87)
book7 = Book.create(title: "Waterfront", author: author2, publisher: pub2, price: 11.97)
book8 = Book.create(title: "Bored as Hell", author: author6, publisher: pub3, price: 10.96)
book9 = Book.create(title: "History of the Silk Road", author: author7, publisher: pub3, price: 8.10)
booka = Book.create(title: "Something for Later", author: author1, publisher: author1, price: 9.54)
bookb = Book.create(title: "What If", author: author3, publisher: author3, price: 13.32)
bookc = Book.create(title: "Lilly Reborn", author: author4, publisher: pub3, price: 16.43)
bookd = Book.create(title: "Anathema", author: author5, publisher: author5, price: 9.41)
booke = Book.create(title: "Best Of", author: author2, publisher: pub3, price: 12.24)
bookf = Book.create(title: "Anyway", author: author6, publisher: pub3, price: 19.99)

Run the command to seed the database.

$ bin/rails db:seed

We should be set!

Enable JSON API and start server

Before we boot we will include Active Model Serializers for JSON:API serialization:

$ echo 'gem "active_model_serializers", "~> 0.10.7"' >> Gemfile
$ bundle

And configure Rails for JSON API serialization in two steps:

  1. Create an initializer at config/initializers/json_api.rb with the following code:

    ActiveSupport.on_load(:action_controller) do
      require 'active_model_serializers/register_jsonapi_renderer'
    ActiveModelSerializers.config.adapter = :json_api
  2. Generate appropriate serializers for each class:

    $ bin/rails g serializer book
    $ bin/rails g serializer author
    $ bin/rails g serializer publishing_house

Run the server and try to fetch data with curl:

$ bin/rails server
=> Booting Puma
=> Rails 5.1.5 application starting in development

# in another tab/window
$ curl http://localhost:3000/publishing_houses

# if you want pretty printing (and have python or jq installed)
$ curl -s http://localhost:3000/publishing_houses | python -m json.tool
$ curl -s http://localhost:3000/publishing_houses | jq '.'

The server should respond something like:

  "data": [
      "id": "1",
      "type": "publishing-houses"
      "id": "2",
      "type": "publishing-houses"
      "id": "3",
      "type": "publishing-houses"

Success? I hope! No other attributes than id are present for now. We will add them in a minute.

Alternatives to active_model_serializers are JSONAPI::Resources, jsonapi-rb and Fast JSON API.


Generate models with Ember CLI:

$ ember generate model book title:string price:number author:belongs-to publisher:belongs-to
$ ember g model author name:string books:has-many
$ ember g model publishing-house name:string discount:number books:has-many

(Polymorphic) Relationships!

Reviewing our model specs we notice that an author can have many books and that he can publish. Publishing houses also have a bunch of published books.

We must ensure our models and serializers are set up correctly. For your convenience, I highlighted the modified lines.

Octane news & best practices, straight to your inbox?

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)


Book model

Nothing to change here. Note that publisher is not a model but a polymorphic interface.

# app/models/book.rb

class Book < ApplicationRecord
 belongs_to :author
 belongs_to :publisher, polymorphic: true


Include author and publisher ids (and type!) in the output.

# app/serializers/book_serializer.rb

class BookSerializer < ActiveModel::Serializer
 attributes :id, :title, :price
 belongs_to :author
 belongs_to :publisher

Author model

Authors that behave as publishers offer a fixed 10% discount. There is no field in the database: we will return a constant.

Here we have to add two has_many :books statements. First one because an author authors books. And the second one because he can publish books. As both are book collections, we will alter the name of the published items and call it published. Rails can't infere those relationship details so we need to tell it where the table and foreign key are located.

The published books are published by a publisher (our polymorphic interface). We associate it using the as keyword.

# app/models/author.rb

class Author < ApplicationRecord
 def discount() 10 end
 has_many :books
 has_many :published, foreign_key: :publisher_id, class_name: 'Book', as: :publisher


Ask the serializer to include authored and published books’ ids. Additionally, add the :discount property.

# app/serializers/author_serializer.rb

class AuthorSerializer < ActiveModel::Serializer
  attributes :id, :name, :discount
  has_many :books
  has_many :published

PublishingHouse model

In Author we changed :books to :published. Likewise, we need to make that change to PublishingHouse.

# app/models/publishing_house.rb

class PublishingHouse < ApplicationRecord
  has_many :published, as: :publisher, foreign_key: :publisher_id, class_name: 'Book'


We simply add the aforementioned published property in order to get those published book ids in our JSON.

# app/serializers/publishing_house_serializer.rb

class PublishingHouseSerializer < ActiveModel::Serializer
  attributes :id, :name, :discount
  has_many :published


An abstract Publisher is necessary to model the polymorphic relationship in Ember.

$ ember g model publisher

Publisher model

Once created, we add all common properties to publishers: name, discount, published.

// app/models/publisher.js

import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { hasMany } from 'ember-data/relationships';

export default Model.extend({
  name: attr(),
  discount: attr('number'),
  published: hasMany('book')
Relationships like published are asynchronous by default.

PublishingHouse model

Not much here. PublishingHouse simply extends Publisher.

// app/models/publishing-house.js

import Publisher from './publisher';

export default Publisher.extend();
Did you notice? In the JSON:API world model names composed of multiple words will be dasherized by default: publishing-house.

Author model

Author extends Publisher, too. The difference with PublishingHouse, is that an author has a collection of authored books.

// app/models/author.js

import Publisher from './publisher';
import attr from 'ember-data/attr';
import { hasMany } from 'ember-data/relationships';

export default Publisher.extend({
  name: attr(),
  books: hasMany()

Book model

The code below explains itself. We have two belongsTo relationships: one to the author and the other one to a publisher, which is polymorphic.

// app/models/book.js

import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { belongsTo } from 'ember-data/relationships';

export default Model.extend({
  title: attr(),
  price: attr('number'),
  author: belongsTo('author', { inverse: 'books' }),
  publisher: belongsTo('publisher', { polymorphic: true, inverse: 'published' })

What is inverse you may ask? Turns out that Author has two book collections: books (authored) and published. Inverse is how Ember Data deals with these ambiguities.

Well, that was a lot! (But we learned a lot, too.) Let's get ready to explore our data from the client.

Exploring data with the Inspector

If you still don't have Ember Inspector go ahead now and grab it.

Let's boot Ember CLI server. We are going to use the --proxy flag to let it know our backend is listening at port 3000.

$ ember server --proxy http://localhost:3000
Proxying to http://localhost:3000
Livereload server on http://localhost:49152
Serving on http://localhost:4200/

Navigate to http://localhost:4200 and open Developer Tools. Go to Ember and click on the Data tab. Leave an active console, too.


Get hold of the application route through the Inspector's $E special variable (here's how). We are particularly interested in accessing its reference to the store.

Loadin’ stuff


Great - now we have all books loaded in our local store.


Time to play around with peekAll, peekRecord, and so on. Try promises on relationships. For example:

$E.store.peekAll('book').objectAt(6).get('author').then(data => alert(data.get('name')))

$E.store.peekRecord('author', 2).get('books').map(value => value.get('title'))

Once you're ready, let's carry on by cleaning up the store:


Octane news & best practices, straight to your inbox?

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)

Saving a new record

In order to save a new book, we will have to configure our controllers to deserialize the incoming parameters – turn them from JSON API into a standard Ruby hash. In this example, our BooksController:

# app/controllers/books_controller.rb

# ...

def book_params
  res = ActiveModelSerializers::Deserialization.jsonapi_parse(params, polymorphic: [:publisher])
  res[:publisher_type] = res[:publisher_type].singularize.capitalize

Read this if you are curious about the modification of the :publisher_type attribute above.

Ember adheres to JSON API and sends payload keys in plural lowercase. For example, authors.

It turns out the AMS key_transform is not very smart with deserialization. Rails does not understand that polymorphic type authors refers to the Author class. Without that singularization/capitalization line, Rails isn't able to find the class and throws an exception.

So, if and only if we wanted to remove the singularization/capitalization above, we could create a serializer

…to adjust the request payload keys so that Rails can understand the types:

Author James Jackson just self-published a new book called “Ember and Coal”. Let's store it in our system.

We know James is author with id 1. In order to find him we will use findRecord which returns a Promise.

var james;
$E.store.findRecord('author', 1).then(author => { james = author });

Alternatively, we could have retrieved James synchronously if we knew he was loaded in the local store. That is what peekRecord is for.

const james = $E.store.peekRecord('author', 1);

Either way, we can use the james object to create his new book. Remember that publisher is polymorphic and will accept a PublishingHouse or an Author. In this case James is publishing his own book, so that will be an Author.

const book = $E.store.createRecord('book', { title: "Ember and Coal", price: 9.99, author: james, publisher: james});


Calling james.get('books') will get us all of his books (2 in total).


Last requirement

Our UX team has asked us to display the (soon to come) author page as fast as possible.

We decided that whenever we load an author, it will bring all its books with it – in the same request. In authors_controller.rb we'll define:

# app/controllers/authors_controller.rb

# GET /authors/1
def show
  render json: @author, include: ['books']

Let's reload the browser and then run in the console:

$E.store.findRecord('author', 4)

We should see the requested author Amanda Djidjinski along with her three books.


(Check the Network panel, only one request was fired!)

I created repositories for both the Bookstore client and the Bookstore API on Github.

Moving forward

In our next instalment, we'll use this data skeleton to work on routes and controllers for the Bookstore. We'll learn to deal with metadata, errors, background fetches, caching via shouldReloadRecord, query pameters and much more.

Were you able to make your app run fine? Any questions or problems along the way? Let me know in the comments!

Update: Part 2 of this guide is up!

Enjoyed this article? Join Snacks!

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)