Ruby on Rails Developer Series: Power of Strong APIs using JSON and Postgres Database

Written by: Evan Glazer
3 min read

Welcome to the second of four Ruby on Rails Developer Series. In this series, the goal is to outline how to strengthen your API with Postgres, how to dockerize your project and add security layers to mitigate attacks to your application. In this article, we'll cycle through strengthening our API-based application that uses the JSON API from Active Model Serializer. Then we'll take advantage of JSON API features.

Let's Begin

The goal of the project has shifted and we're now advancing our basic CRUD application into an application that allows for a user to have a to-do list. We need to be able to support showing todo cards in our iOS application - 10 cards at a time specifically. So in this project, we will use paging techniques to optimize the way we fetch JSON data.

Let's start by creating an Exception Handler to include in our Application Base Controller.

This will help us take care of any record issues we may face and catch/output the response back to the application requesting data.

app/controllers/concerns/exception_handler.rb
module ExceptionHandler
  extend ActiveSupport::Concern
  included do
    rescue_from ActiveRecord::RecordNotFound do |e|
      render json: { message: e.message }, status: 404
    end
    rescue_from ActiveRecord::RecordInvalid do |e|
      render json: { message: e.message }, status: 422
    end
  end
end

Now we can remove: Line 2 - rescue_from ActiveRecord::RecordNotFound, with: :record_not_found and the method record_not_found from our Users Controller.

We will also want to include our Exception Handler Concern in our Base Controller.

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include ExceptionHandler
end

Now that we have stronger exception handlers for our controllers, we can focus on the models we need to create in order to achieve our project requirements.

The way our logic is going to flow:

Let's begin to generate the models:

rails g model Todo name:string slug:string priority:string completed:boolean user_id:integer
  invoke  active_record
  create    db/migrate/20190615221743_create_todos.rb
  create    app/models/todo.rb
  invoke    test_unit
  create      test/models/todo_test.rb
  create      test/fixtures/todos.yml
rails g model Item name:string slug:string priority:string position:integer completed:boolean todo_id:integer`
  invoke  active_record
  create    db/migrate/20190615221812_create_items.rb
  create    app/models/item.rb
  invoke    test_unit
  create      test/models/item_test.rb
  create      test/fixtures/items.yml

Now let's migrate our tables into our db.

rake db:migrate

And we need to add the associations to our Models:

class User < ActiveRecord::Base
  has_many :todos
end
class Todo < ActiveRecord::Base
  belongs_to :user
  has_many :items
end
class Item < ActiveRecord::Base
  belongs_to :todo
end

Ok, where are we at now?

Seed some data in our database from ../db/seeds.rb:

# Create our user
user = User.create(first_name: 'Evan', last_name: 'Glazer', email: 'evanowner@live.com')
# Each Todo has 10 Items associated with it
100.times.each do |i|
  todo = Todo.create(name: "Todo #{i}", slug: "todo-#{i}", priority: 'medium', completed: false, user_id: user.id)
  10.times.each do |k|
    Item.create("Item #{k}", slug: "Item-#{k}", priority: 'low', position: j, completed: false, todo_id: todo.id)
  end
end

Then let's run rake db:seeds to get our data in the db.

Try It - We should be able to now access all our associations properly

user = User.first
user.todos.first.items

Things we will see added and changed for better practices and security measures in the next articles of this series:

* Devise - is a flexible authentication solution for Rails based on Warden.
* Validations to ensure the data we receive and create is consistent.
* FriendlyId is the "Swiss Army bulldozer" of slugging and permalink plugins for Active Record. It lets you create pretty URLs and work with human-friendly strings as if they were numeric ids.
* Controller Testing

Here we're building our TodoController (\app\controllers\api\v1\todos_controller.rb):

class API::V1::TodosController < ApplicationController
  def index
    todos = Todo.where(user_id: params[:user_id])
    render json: todos, each_serializer: TodoSerializer, status: 200
  end
  def show
    todo = Todo.find_by!(user_id: params[:user_id], id: params[:id])
    render json: todo, each_serializer: TodoSerializer, status: 200
  end
  def create
    todo = Todo.create!(todo_params)
    render json: todo, each_serializer: TodoSerializer, status: 200
  end
  def destroy
    todo = Todo.find_by!(user_id: params[:user_id, id: params[:id]]).destroy!
    render json: todo, each_serializer: TodoSerializer, status: 200
  end
end

Then theTodoSerializer(\app\serializers\todo_serializer.rb) :

class TodoSerializer < ActiveModelSerializers::Model
  type :todo
  attributes :first_name, :last_name, :email
  has_many :items
end

Time to turn the page! (Just kidding)

Next, let's implement our paging technique to our controller for fetching 10 records at a time from our users' todo lists:

Install this gem: https://github.com/mislav/will_paginate
Add to Gemfile `gem 'will_paginate', '~> 3.1.0'`
Then `bundle install`
Now we want to change our index method in our `TodosController` to paginate.

The iOS application will need to keep track of the pages its calling and increment as the user scrolls, etc. Below is setting our per page limit to show 10 Todo lists at a time when we perform a paginated query.

 def index
    todos = Todo.where(user_id: 1).paginate(page: 1, per_page: 10)
    render json: todos, each_serializer: TodoSerializer, status: 200
  end

Finish Line

In this part of the series, we have implemented the ability for our API to show todo cards in our iOS application - 10 cards at a time, specifically. We strengthened our exception handlers to properly respond to the iOS application. And added paging techniques to work within our JSON API Serializers. Now you could have built something like this:

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.