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 HTTP.
Currently three main API Styles are used on the Web:
This Guide is concerned with REST, there is a second guide for GraphQL. SOAP is rearely offered with Rails, but there is a soap client in ruby.
Please note that any of the API styles can be used with any backend, frontend, persistance layers:
That's kind of the point of an API: to allow different technologies on both sides of the API.
Using an API you can build a common backend for different cleints:
Your "backend" might still offer some "Server Rendered HTML" besides the API:
The acronym REST was coined by Roy Fielding in his dissertation. When describing the architecture of the web, and what made it so successful on a technical level, he described this architecture as "Representational State Transfer" (REST).
This Acronym was later picked up to describe a certain style of API, and to distinguish 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:
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 HTTP is a quick read, and well worth it!
References for status codes:
When building 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 Scissorhands hat insgesamt 4 Werke in dieser Datenbank.
Er hat den Usernamen fhs123.</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>Scissorhands</nachname>
<username>fhs123</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":"Edvard",
"nachname":"Scissorhands",
"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. Statelessness makes caching easy. And in a scenario with multiple servers behind a load balancer, having no state on the server means that the application will work when a client's requests are routed 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 imlements the HATEOS aspect of REST and defines a way to do associations and aggregation.
For smaller projects it might be overkill.
You can explor a REST with several tool:
The OpenAPI Specification is a way for specifying and documenting REST APIs. There are a lot of tools available around it for many different programming languages.
For Rails I recommend the gem rswag
: with rswag you write tests (specs) for your
api, and the documentation is generated from the (successful) tests.
There is also a web-ui to read the documentation and run API requests -
Swagger Web UI in the example app
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 blueprint
gem for creating JSON output. It does not comply with
the JSON-API specification, but it is fine for smaller projects.
bundle add 'blueprinter'
Beware: After adding a gem you need to restart the rails server!
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::V0::BaseController
def index
users = User.all
render json: ...
end
def show
user = User.find(params[:id])
render json: ...
end
end
The controller loads the right model, and then needs to calls a serializer to do the actual rendering of the json data. We will create this serializer next.
With the gem blueprinter the
serializers live in the /app/blueprints
folder.
# app/blueprints/user_blueprint.rb
class UserBlueprint < Blueprinter::Base
identifier :id
fields :name, :email
end
This serializer can be used both for single users and for arrays of users. We can now complete the controller:
# app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < Api::V0::BaseController
def index
users = User.all
render json: UserBlueprint.render(users)
end
def show
user = User.find(params[:id])
render json: UserBlueprint.render(user)
end
end