Create a boilerplate Rails project
First generate a new rails api:
$ rails new rails-jsonapi \
--database=postgresql \
--skip-action-mailbox \
--skip-action-text \
--skip-spring -T \
--skip-turbolinks \
--api
$ cd rails-jsonapi
This will create a boilerplate Rails api project using postgresql
as a database with a few things removed to keep it concise.
Next, generate the models and controllers:
rails g resource Author name:string --no-test-framework
rails g resource Article title:string body:text author:references --no-test-framework
Don't forget to add the has_many
macro to the author model to complete the association.
# app/models/author.rb
class Author < ApplicationRecord
has_many :articles
end
Add some seed data to get started.
bundle add faker
# db/seeds.rb
require 'faker'
Author.delete_all
Article.delete_all
10.times {
Author.create( name: Faker::Book.unique.author)
}
50.times {
Article.create({
title: Faker::Book.title,
body: Faker::Lorem.paragraphs(number: rand(5..7)),
author: Author.limit(1).order("RANDOM()").first # sql random
})
}
Setup the database, run migrations and generate seed data.
rails db:create db:migrate db:seed
Setup the Api
endpoint and controller
Wrap the generated routes in a scope block to add /api
as the base route for all routes nested with it. Don't worry about explicitly defining controller actions at this point.
# config/routes.rb
Rails.application.routes.draw do
scope :api do
resources :articles
resources :authors
end
end
Setup ArticleController
and AuthorController
with basic actions :index
and :show
, called by GET
requests to /api/articles
and /api/authors
, respectively.
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
before_action :find_article, only: :show
def index
@articles = Article.all
render json: @articles
end
def show
render json: @article
end
private
def find_article
@article = Article.find(params[:id])
end
end
# app/controllers/author_controller.rb
class AuthorsController < ApplicationController
before_action :find_author, only: :show
def index
@authors = Author.all
render json: @authors
end
def show
render json: @author
end
private
def find_author
@author = Author.find(params[:id])
end
end
At this point we have a working api that response to requests with json! Any request sent to GET /articles
However, we haven't implemented any of our associations into our response logic, so the response body for GET /articles
does not include any author data.
[
{
"id": "1",
"type": "article",
"attributes": {
"title" "Where the Red Fern Grows",
"body": "...",
"author_id": 2,
}
}
]
We could solve this by changing the render lines to include the associations...
...
def index
@articles = Article.all
render json: @articles, include: [:author]
end
...but this can get cumbersome very quickly, and is not DRY at all. There are many ways to solve this issue, one of which being the active_model_serializers
gem. If you just need to wrangle your json responses, this is a a viable option.
Enter Fast JSONapi
Fast JSONapi is a Ruby library created by the Netflix development team. It includes a serializer that implements the full https://jsonapi.org/ spec. This will introduce a bit more complexity in both the frontend and the backend, but the performance benefits can easily outweigh all of that, depending on your situation. You can learn more about fast_jsonapi's performance benchmarks https://github.com/Netflix/fast_jsonapi/blob/master/performance_methodology.md.
Add the fast_jsonapi
gem to the project.
bundle add 'fast_jsonapi'
We can now use the serializer generator that is bundled with fast_jsonapi
.
rails g serializer Article title body
rails g serializer Author name
This will create two files:
# app/serializers/article_serializer.rb
class ArticleSerializer
include FastJsonapi::ObjectSerializer
attributes :title, :body
end
# app/serializers/author_serializer.rb
class AuthorArticleSerializer
include FastJsonapi::ObjectSerializer
attributes :name
end
To keep it simple, we will only define the associations on the ArticleSerializer
.
# app/serializers/article_serializer.rb
class ArticleSerializer
include FastJsonapi::ObjectSerializer
attributes :title, :body
belongs_to :author
end
Implement the serializers in their respective controllers.
# app/controllers/authors_controllers.rb
class AuthorsController < ApplicationController
before_action :find_author, only: :show
def index
@authors = Author.all
options = { include: [:articles]}
render json: AuthorSerializer.new(@authors, options).serializable_hash
end
def show
options = { include: [:articles]}
render json: AuthorSerializer(@author, options).serializable_hash
end
private
def find_author
@author = Author.find(params[:id])
end
end
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
before_action :find_article, only: :show
def index
@articles = Article.all
options = { include: [:author]}
render json: ArticleSerializer.new(
@articles.preload(:author),
options
).serializable_hash
end
def show
options = {:include => [:author]}
render json: ArticleSerializer.new(@article, options).serializable_hash
end
private
def find_article
@article = Article.find(params[:id])
end
end
Run rails s
to start up the rails server.
If you're not already using a rest client such as Insomnia or Postman, get with the times! Make a GET
request to localhost:3000/articles
.
The response should look similar to this:
{
"data": [
{
"id": "1",
"type": "article",
"attributes": ...,
"relationships": {
"author": {
"data": {
"id": "9",
"type": "author"
}
}
}
},
{
"id": "2",
"type": "article",
"attributes": ...,
"relationships": {
"author": {
"data": {
"id": "3",
"type": "author"
}
}
}
},
{
"id": "3",
"type": "article",
"attributes": ...
"relationships": {
"author": {
"data": {
"id": "3",
"type": "author"
}
}
}
}
],
"included": [
{
"id": "9",
"type": "author",
"attributes": {
"name": "Tawna Denesik PhD"
}
},
{
"id": "3",
"type": "author",
"attributes": {
"name": "Mrs. Carmela Herzog"
}
}
]
}
Top comments (3)
It was really useful. I didn't understand why should I use fastjson. Thks.
Thanks Carlos! I'm going to add more to this article soon as I feel I didn't do the tech enough justice.
Rails doesn't render json api by default, so the first response you have up there isn't right. It isn't until you install the fast_jsonapi gem.
Also the serializer is missing "has_many :articles".