Skip to content

Endpoints Basic API

sergio edited this page Dec 18, 2020 · 24 revisions

We can use multiple routes to differentiate between specific requests. In an API, these are typically referred to as endpoints. A user of the API uses endpoints to access specific pieces of data. Just like a normal Rails app, we can create full CRUD based controllers that only render JSON.

Rails.application.routes.draw do
  get '/birds' => 'birds#index'
  get '/birds/:id' => 'birds#show' # new
end
class BirdsController < ApplicationController
  def index
    birds = Bird.all
    render json: birds
  end

  def show
    bird = Bird.find_by(id: params[:id])
    render json: bird
  end
end

An API that sends user information might contain details of a user internally that it does not want to ever share externally.

{
  "id": 2,
  "name": "Grackle",
  "species": "Quiscalus Quiscula",
  "created_at": "2019-05-09T21:51:41.543Z",
  "updated_at": "2019-05-09T21:51:41.543Z"
}

We probably don't need bits of data like created_at and updated_at. Rather than send this unnecessary info when rendering, we could just pick and choose what we want to send:

def show
  bird = Bird.find_by(id: params[:id])
  render json: {id: bird.id, name: bird.name, species: bird.species } 
end

.slice() simplifies it:

def show
  bird = Bird.find_by(id: params[:id])
  render json: bird.slice(:id, :name, :species)
end

both methods above results in:

{
  "id": "3",
  "name": "Common Starling",
  "species": "Sturnus Vulgaris"
}

what if we have collection of hashes?

def index
  birds = Bird.all
  render json: birds
end

only:

def index
  birds = Bird.all
  render json: birds, only: [:id, :name, :species]
end

except:

def index
  birds = Bird.all
  render json: birds, except: [:created_at, :updated_at]
end

Error Messages

def show
  bird = Bird.find_by(id: params[:id])
  if bird
    render json: { id: bird.id, name: bird.name, species: bird.species }
  else
    render json: { message: 'Bird not found' }
  end
end

Now, if we were to send a request to an invalid endpoint like http://localhost:3000/birds/hello_birds, rather than receiving a general HTTP error, we would still receive a response from the API:

{
  "message": "Bird not found"
}

Using include

In this lesson, we will look at how our Rails API will be able to convey relationships between multiple models in a single JSON object.

def show
  sighting = Sighting.find_by(id: params[:id])
  render json: sighting
end

visiting http://localhost:3000/sightings/1:

{
  "id": 1,
  "bird_id": 1,
  "location_id": 2,
  "created_at": "2019-05-14T11:20:37.225Z",
  "updated_at": "2019-05-14T11:20:37.225Z"
}

NOTE: Notice that the object includes its own "id", as well as the related "bird_id" and "location_id". That is useful data. We could use these values to send additional requests using JavaScript to get bird and location data if needed.

def show
  sighting = Sighting.find_by(id: params[:id])
  render json: { id: sighting.id, bird: sighting.bird, location: sighting.location }
end
{
  "id": 2,
  "bird": {
    "id": 2,
    "name": "Grackle",
    "species": "Quiscalus Quiscula",
    "created_at": "2019-05-14T11:20:37.177Z",
    "updated_at": "2019-05-14T11:20:37.177Z"
  },
  "location": {
    "id": 2,
    "latitude": 30.26715,
    "longitude": -97.74306,
    "created_at": "2019-05-14T11:20:37.196Z",
    "updated_at": "2019-05-14T11:20:37.196Z"
  }
}

An alternative is to use the include option to indicate what models you want to nest:

def show
  sighting = Sighting.find_by(id: params[:id])
  render json: sighting, include: [:bird, :location]
end

 #also works fine when dealing with an action that renders an array
def index
  sightings = Sighting.all
  render json: sightings, include: [:bird, :location]
end
{
  "id": 2,
  "bird_id": 2,
  "location_id": 2,
  "created_at": "2019-05-14T11:20:37.228Z",
  "updated_at": "2019-05-14T11:20:37.228Z",
  "bird": {
    "id": 2,
    "name": "Grackle",
    "species": "Quiscalus Quiscula",
    "created_at": "2019-05-14T11:20:37.177Z",
    "updated_at": "2019-05-14T11:20:37.177Z"
  },
  "location": {
    "id": 2,
    "latitude": 30.26715,
    "longitude": -97.74306,
    "created_at": "2019-05-14T11:20:37.196Z",
    "updated_at": "2019-05-14T11:20:37.196Z"
  }
}

To get more control of the data shown also in the nested Objects:

def show
  sighting = Sighting.find_by(id: params[:id])
  render json: sighting, include: {
    bird: {only: [:name, :species]},
    location: {only: [:latitude, :longitude]}
  }, except: [:updated_at]
end
{
  "id": 2,
  "bird_id": 2,
  "location_id": 2,
  "created_at": "2019-05-14T11:20:37.228Z",
  "bird": {
    "name": "Grackle",
    "species": "Quiscalus Quiscula"
  },
  "location": {
    "latitude": 30.26715,
    "longitude": -97.74306
  }
}

For the purposes of building applications in JavaScript and frameworks like React we specifically need Rails to act as an API that responds with JSON.

When first building a Rails application, it is possible to flag that the application should be API-only.

rails new bird-watcher-api --api

as example we could continue with:

rails g resource bird name species
rails g resource location latitude longitude
rails g resource sighting bird:references location:references

then

def index
  sightings = Sighting.all
  render json: sightings, include: [:bird, :location]
end

since we used resource we will need to clean up config/routes.rb


controllers are really just meant to act as a relay between our models and our view, or well, our rendered JSON in this case. If we can extract the work of customizing our JSON data and put it somewhere else, we could keep our controller actions cleaner.

In the SightingsController, we already have working render statements. Our goal is not to change these statements, just move the work off of the controller.

To create a class we will be able to utilize in place of the current render statements, first, we'll create a new folder within app called services:

mkdir app/services
touch app/services/sighting_serializer.rb
class SightingSerializer

  def initialize(sighting_object)
    @sighting = sighting_object
  end

  def to_serialized_json
    options = {
      include: {
        bird: {
          only: [:name, :species]
        },
        location: {
          only: [:latitude, :longitude]
        }
      },
      except: [:updated_at],
    }
    @sighting.to_json(options)
  end

end

Needs to restart the Rails server.

once we have that SightingSerializer we can replace this code:

#class SightingsController < ApplicationController
#  def index
#    sightings = Sighting.all
#    render json: sightings.to_json(:include => {
#      :bird => {:only => [:name, :species]},
#      :location => {:only => [:latitude, :longitude]}
#    }, :except => [:updated_at])
#  end
#
#  def show
#    sighting = Sighting.find_by(id: params[:id])
#    render json: sighting.to_json(:include => {
#      :bird => {:only => [:name, :species]},
#      :location => {:only => [:latitude, :longitude]}
#    }, :except => [:updated_at])
#  end
#end
## by this code:

class SightingsController < ApplicationController
  def index
    sightings = Sighting.all
    render json: SightingSerializer.new(sightings).to_serialized_json
  end

  def show
    sighting = Sighting.find_by(id: params[:id])
    render json: SightingSerializer.new(sighting).to_serialized_json
  end
end

To use a gem to standardise this process

gem 'fast_jsonapi'

will create new generators:

rails g serializer Bird
rails g serializer Location
rails g serializer Sighting
app/serializers/bird_serializer.rb
app/serializers/location_serializer.rb
app/serializers/sighting_serializer.rb
class BirdSerializer
  include FastJsonapi::ObjectSerializer
  attributes :name, :species
end
class LocationSerializer
  include FastJsonapi::ObjectSerializer
  attributes :latitude, :longitude
end
class SightingSerializer
  include FastJsonapi::ObjectSerializer
  attributes :bird, :location
end
class SightingsController < ApplicationController
  def show
    sighting = Sighting.find(params[:id])
    render json: SightingSerializer.new(sighting)
  end

  def index
    sightings = Sighting.all
    render json: SightingSerializer.new(sightings)
  end
end

However, here, we have no control over what attributes are included in the related objects, and so we get all the attributes of "bird" and "location".

To achieve that control and add relationships, we need two steps, first include relationship into the serializer, then use an options parameter hash when initialising the serialiser in controller:

  class SightingSerializer
    include FastJsonapi::ObjectSerializer
    attributes
    belongs_to :bird
    belongs_to :location
  end
def show
  sighting = Sighting.find_by(id: params[:id])
  options = {
    include: [:bird, :location]
  }
  render json: SightingSerializer.new(sighting, options)
end

Using Fast JSON API, with the use of relationships and passing a second parameter, we are able to get the same data, but in a much different structure than by writing our own serializers.

NOTE: Serializers generated by the Fast JSON API gem have two built-in methods called serializable_hash and serialized_json which return a serialized hash and a JSON string respectively. However, we don't actually need either of these in this example, as to_json will still be called on SightingSerializer.new(sighting) implicitly.


# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
# gem 'rack-cors'

To get rack-cors working, uncomment the gem and run bundle install. Then, add the following to config/application.rb inside class Application < Rails::Application:

 config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins '*'
        resource '*',
          :headers => :any,
          :methods => [:get, :post, :delete, :put, :patch, :options, :head],
          :max_age => 0
      end
    end

This shouldn't replace anything else inside class Application < Rails::Application, just be included in addition.

This will allow you to test your APIs while developing them locally.



We can now take a single model or all the instances of that model and render it to JSON, extracting out any specific content we do or do not want to send!

Whether you are building a professional API for a company or for your own personal site, having the ability to fine tune how your data look is a critical skill that we're only just beginning to scratch the surface on.


class UserSerializer
  def initialize(user)
    @user=user
  end

  def to_serialized_json(*additional_fields)
    options ={
      only: [:username, :id, *additional_fields]
    }

    @user.to_json(options)
  end
end

each time you want to add new more fields to be serialized, you can do something like

#say this is in user_controller.rb

UserSerializer.new(@user).to_serialized_json(:token, :errors)

Or if you want to add an hash instead:

class UserSerializer
  def initialize(user)
    @user=user
  end

  def to_serialized_json(**additional_hash)
    options ={
      only: [:username, :id]
    }

    @user.as_json(options).merge(additional_hash)
  end
end

Clone this wiki locally