Build a Highly Performant Api with Rails 6 and fast_jsonapi
Tags: ruby, rails
Published: 2020-06-19 17:59:57

## Create a boilerplate Rails project

First generate a new rails api:
```shell
$ 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:
```shell
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.
```ruby
# app/models/author.rb
class Author < ApplicationRecord
    has_many :articles
end
```

Add some seed data to get started.
```shell
bundle add faker
```

```ruby
# 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.
```shell
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.
```ruby
# 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.
```ruby
# 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.
```json
[
  {
    "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...
```ruby
...
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/](JSON:API) 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](here).

Add the `fast_jsonapi` gem to the project.
```shell
bundle add 'fast_jsonapi'
```
We can now use the serializer generator that is bundled with `fast_jsonapi`.
```shell
rails g serializer Article title body
rails g serializer Author name
```

This will create two files:
```ruby
# app/serializers/article_serializer.rb
class ArticleSerializer
  include FastJsonapi::ObjectSerializer
  attributes :title, :body
end
```
```ruby
# 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`. 
```ruby
# app/serializers/article_serializer.rb
class ArticleSerializer
  include FastJsonapi::ObjectSerializer
  attributes :title, :body
  belongs_to :author
end
```

Implement the serializers in their respective controllers.
```ruby
# 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
```


```ruby
# 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](https://insomnia.rest/) or [Postman](https://www.postman.com/), get with the times! Make a `GET` request to `localhost:3000/articles`.

The response should look similar to this:
```json
{
  "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"
      }
    }
  ]
}
```