DEV Community

Cover image for Build a Highly Performant Api with Rails 6 and fast_jsonapi
Patrick Jones
Patrick Jones

Posted on • Updated on

Build a Highly Performant Api with Rails 6 and fast_jsonapi

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"
      }
    }
  ]
}

Discussion (3)

Collapse
carlosvazquez profile image
Carlos Vazquez

It was really useful. I didn't understand why should I use fastjson. Thks.

Collapse
forksofpower profile image
Patrick Jones Author

Thanks Carlos! I'm going to add more to this article soon as I feel I didn't do the tech enough justice.

Collapse
ghostreef profile image
ghostreef

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".