After working through this guide you will:
Slides - use arrow keys to navigate, esc to return to page view, f for fullscreen
API stands for "Application Programming Interface". It is a set of clearly defined methods of communication with a software component. So the objects and methods exposed by a library form an API.
In Web development the acronym API is most commonly used when the software component in question runs on a different server on the Internet and is accessed via the network.
This Guide is concerned with APIs that you build and run using Ruby on Rails.
The acronym REST was coined by Roy Fielding in his dissertation. When describing the architecture of the web, and what made it so successfull on a technical level, he desribed this architecture as "Representational State Transfer" (REST).
This Acronym was later picked up to describe a certain style of API, and to distiguish such APIs from SOAP APIs.
A REST API allows to access and manipulate textual representations of Web resources using HTTP Methods and stateless operations.
"Web resources" were first defined on the World Wide Web as documents or files identified by their URLs, but today they have a much more generic and abstract definition encompassing every thing or entity that can be identified, named, addressed or handled, in any way whatsoever, on the Web.
Tilkov(2007) gives a brief introduction to REST. The main points are:
TODO
Give every resource a unique URL. Please note that REST does not demand a certain form of URL. While URLs with no parameters are often used:
https://example.com/users/
https://example.com/users/1/
https://example.com/users/2/
https://example.com/users/3/
it is just as restful to use parameters:
https://example.com/users.php
https://example.com/users.php?id=1
https://example.com/users.php?id=2
https://example.com/users.php?id=3
In REST, the URLs correspond to resources, which are represented by nouns. This is a difference to SOAP, there there is typically just one endpoint:
https://example.com/soap/router/
Through this endpoint you can access methods like getUserData()
or deleteUser()
.
“Hypermedia as the engine of application state” means that a client interacts with a network application entirely through hypermedia, and needs no prior knowledge of URLs.
If an API returns the following JSON:
{
"id": "1",
"name": "Example User",
"email": "example@railstutorial.org"
"profile_pics": [ 2, 5 ]
}
Then the Client needs to know how to get profile_pics from the API. For example because the developer read the docs.
HATEOAS demands that the full URL is used to refer to other resources:
{
"id": "1",
"name": "Example User",
"email": "example@railstutorial.org"
"profile_pics": [
"https://sample.com/api/profile/pictures/2",
"https://sample.com/api/profile/pictures/5"
]
}
Use HTTP Methods (and Status Codes) as intended.
Regarding the HTTP Methods there are two important distinctions:
The definition of the Methods in HTTP1.0 is just a short read, and worth its while!
References for status codes:
When buidling a REST API, the HTTP Protocol already defines a lot about that API. There is no need to come up with a way to delete a resource, or to indicate failure. HTTP already offers the DELETE method and status codes that indicate errors.
The same resource can be available in different formats. There are two common ways of requesting different formats:
With the HTTP Header Accept
:
GET /mini/person/83 HTTP/1.1
Host: example.com
Accept: application/xml
Or by adding an "extension" as part of the URL:
https://example.com/mini/person/83.html
https://example.com/mini/person/83.xml
https://example.com/mini/person/83.json
The three different versions of person number 83 might look like this: the HTML web page:
<h1>Details zu einer Person</h1>
<p><img src="https://example.com/mini/profil/edvard_1_2.jpg" />
Herr Edvard Paul Beisteiner hat insgesamt 4 Werke in dieser Datenbank.
Er hat den Usernamen fhs14287.</p>
<ul>
<li><a href='https://example.com/mini/werk/24'>The Thin Red Line</a></li>
<li><a href='https://example.com/mini/werk/50'>Der böse Wolf</a></li>
<li><a href='https://example.com/mini/werk/83'>nimm zwei, schatz</a></li>
<li><a href='https://example.com/mini/werk/303'>the neighbour.</a></li>
</ul>
For an API the same resource might be represented as XML:
<person>
<image ref='https://example.com/mini/profil/edvard_1_2.jpg' />
<vorname>Edvard</vorname>
<nachname>Beisteiner</nachname>
<username>fhs14287</username>
<werke>
<werk ref='https://example.com/mini/werk/24'>The Thin Red Line</werk>
<werk ref='https://example.com/mini/werk/50'>Der böse Wolf</werk>
<werk ref='https://example.com/mini/werk/83'>nimm zwei, schatz</werk>
<werk ref='https://example.com/mini/werk/303'>the neighbour.</werk>
</werke>
</person>
or as JSON:
{"image":"https://example.com/mini/profil/edvard_1_2.jpg",
"vorname":"Eduard",
"nachname":"Beisteiner",
"werk":[
{"titel":"The Thin Red Line",
"url":"https://example.com/mini/werk/24"},
{"titel":"Der böse Wolf",
"url":"https://example.com/mini/werk/50"},
{"titel":"nimm zwei, schatz",
"url":"https://example.com/mini/werk/83"},
{"titel":"the neighbour.",
"url":"https://example.com/mini/werk/303"}]}
Tilkov wirtes: "REST mandates that state be either turned into resource state, or kept on the client. In other words, a server should not have to retain some sort of communication state for any of the clients it communicates with beyond a single request."
This is important for performance and scalability and performance. Statelessness makes caching easy. And in a scenario with serveral servers behind a load balancer, not having state on the server means the application will work if the requests bei one client are routest to different servers.
When an API returns JSON data this could take many forms. The json:api specification is a well thought out convention for this.
It is especially good with the HATEOS aspect of REST, the json:api specification adhers to this principle.
GraphQL is different way of writing APIs. GraphQL is less concerned with HTTP, it just uses the POST method for all requests.
see
GraphQL Playground
example query:
{
cityZone(id: "2349ksj0342" ) {
id
url
}
}
a possible resonse
{
"data": {
"cityZone": {
"id": "2349ksj0342",
"url": "wuppertal"
}
}
}
Rails is equipped to not just create HTML as output, but to easily offer other representations as well.
When you look at rails routes
you can see that the routes created by
resource :user
could contain an optional format
:
Prefix Verb URI Pattern Controller#Action
root GET / static_pages#home
users GET /users(.:format) users#index
user GET /users/:id(.:format) users#show
Only HTML is implemented by default. But we could use this feature to have other formats:
/users
/users.json
/users.xml
/users/1
/users/1.json
/users/1.xml
When you try out accessing /users/1.json
you get a response:
406 Not Acceptable
Content-Length: 39
Content-Type: application/json; charset=utf-8
{"status":406,"error":"Not Acceptable"}
This error message is meant for a client expecting JSON data. It uses both the HTTP status code and the JSON to indicate the error.
The "Frontend 1" in the example app expects a very simple JSON structure:
To display one user, it loads from /user/1.json
and expects
a single JSON object with three attributes:
{
"id":1,
"name":"Example User",
"email":"example@railstutorial.org"
}
To display the table of users, it loads from /users.json
and
expects a JSON array of objects like above:
[
{
"id":2,
"name":"Precious Heaney",
"email":"example-1@railstutorial.org"
},
{
"id":3,
"name":"Warren Considine Sr.",
"email":"example-2@railstutorial.org"
}
]
The scaffold generator always adds handling JSON responses to the actions of a controller.
For handling just HTML only this code would be needed in the create action:
# POST /users
def create
@user = User.new(user_params)
if @user.save
redirect_to @user, notice: 'User was successfully created.'
else
render :new
end
end
end
But the scaffold generator also adds resond_to
and format
commands,
to handle json differently from html:
# POST /users
# POST /users.json
def create
@user = User.new(user_params)
respond_to do |format|
if @user.save
format.html {
redirect_to @user, notice: 'User was successfully created.'
}
format.json {
render :show, status: :created, location: @user
}
else
format.html {
render :new
}
format.json {
render json: @user.errors, status: :unprocessable_entity
}
end
end
end
so in the controller we might not need to change anything to add the API, only in the view.
We could create views using erb in app/views/users/show.json.erb
:
{
"id": <%= @user.id %>,
"name": "<%= @user.name %>",
"email": "<%= @user.email %>"
}
and app/views/users/index.json.erb
:
[
<%
@users.each_with_index do |user| %>
{
"id": <%= user.id %>,
"name": "<%= user.name %>",
"email": "<%= user.email %>"
},
<% end %>
]
But wait, there's a problem: there is a comma after each object, but there should be no comma after the last.
[
<%
max = @users.length - 1
@users.each_with_index do |user,i| %>
{
"id": <%= user.id %>,
"name": "<%= user.name %>",
"email": "<%= user.email %>"
}
<%= if i < max then ',' end %>
<% end %>
]
And wait, there's another problem: What happens if a users name contains a quote? For example Jack "the Ripper". That would break our current view, because we don't do proper escaping.
Rails 5 comes with the gem jbuilder
which helps you create JSON, and
which handles all the escaping and formatting correctly.
We need to name the view app/views/users/show.json.jbuilder
,
and then can use the the following code to extract three properties
from the user object:
json.id @user.id
json.name @user.name
json.email @user.email
There is also a shorthand for this:
json.extract! @user, :id, :name, :email
For the index view we want to create a JSON array.
In app/views/users/index.json.jbuilder
we write:
json.array! @users do |user|
json.extract! user, :id, :name, :email
end
All the authentication and access control we built into the rails app before is still applicable to the API and JSON views. If the "Frontend" that is using our API is displayed in a browser, the handling of cookies and the session is exactly the same as before.
If the "Frontend" is not in a browser, but is a native mobile app or just another server side job, we have to use an alternative to cookies. JSON Web Tokens are a solution.
To create a stand alone API we define new, separate routes under /api/v1
.
namespace :api do
namespace :v1 do
resources :users, only: [:index, :create, :show, :update, :destroy]
end
end
we will be using the fast_jsonapi
gem for creating JSON output that compllies to jsonapi:
bundle add 'fast_jsonapi'
Beware: After adding a gem you need to restart the rails server!
The "Frontend 2" in the example app expects the json to be formed according to the json api specification.
/api/v1/user/1
will return data about one resource:
{
"data": {
"id": "1",
"type": "users",
"attributes": {
"name": "Example User",
"email": "example@railstutorial.org"
}
}
}
/api/v1/users/
returns an array, but the top level JSON structure
is an object with on attribute data
:
{
"data": [
{
"id": "2",
"type": "users",
"attributes": {
"name": "Precious Heaney",
"email": "example-1@railstutorial.org"
},
},
...
]
}
After we defined the routes, we next need to create a controller.
As we are setting up a new hierarchy of controllers that will only
concerned with the API, it makes sense to inhert from ActionController::API
,
not from ActionController::Base
.
All the "normal" controllers first inhert from ApplicationController
. We
will build a similar structure for the api controllers, the will inhert from
Api::V1::BaseController
:
# app/controllers/api/v1/base_controller.rb
class Api::V1::BaseController < ActionController::API
end
The users controller is the one that's actually called by the route:
# app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < Api::V1::BaseController
def index
users = User.all
render json: UserSerializer.new(users).serialized_json
end
def show
user = User.find(params[:id])
render json: UserSerializer.new(user).serialized_json
end
end
The controller loads the right model, and then calls a serializer to do the actual rendering of the json data.
All serializers live in the /app/serializers
folder.
You can generate a serializer from an existing model:
rails g serializer User name email
this creates the following code:
# app/serializers/user_serializer.rb
class UserSerializer
include FastJsonapi::ObjectSerializer
attributes :name, :email
end
See Halliday(2016): Producing Documentation for Your Rails API for a discussion of automatic methods of documentation generation.