Repository navigation
Endpoints Basic API
- using
onlyandexcept - Using
include - Using the
--apiflag - Creating a Service Class
- EXAMPLE: API from scratch
- How to Disable CORS
- Adding Attributes to JSON Serializer
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
endclass BirdsController < ApplicationController
def index
birds = Bird.all
render json: birds
end
def show
bird = Bird.find_by(id: params[:id])
render json: bird
end
endAn 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)
endboth 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
enddef index
birds = Bird.all
render json: birds, only: [:id, :name, :species]
enddef index
birds = Bird.all
render json: birds, except: [:created_at, :updated_at]
enddef 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
endNow, 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"
}
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
endvisiting 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]
endsince 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
endNeeds 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
endTo 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
endclass LocationSerializer
include FastJsonapi::ObjectSerializer
attributes :latitude, :longitude
endclass SightingSerializer
include FastJsonapi::ObjectSerializer
attributes :bird, :location
endclass 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
endHowever, 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
enddef show
sighting = Sighting.find_by(id: params[:id])
options = {
include: [:bird, :location]
}
render json: SightingSerializer.new(sighting, options)
endUsing 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
endThis 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
endeach 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