1 What is an API
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.
1.1 SOAP, REST and GraphQL
Currently three main API Styles are used on the Web:
- SOAP, designed 1998 at Microsoft, uses XML and POST requests to make "remote procedure calls"
- REST, described in 2000, uses different HTTP Methods and Status Messages to access "resources"
- GraphQL, released 2015 by Facebook, uses POST requests, it's own query language and JSON
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.
1.2 API separates two layers
Please note that any of the API styles can be used with any backend, frontend, persistance layers:
- You can build a REST in front of a PHP backend using MongoDB as the database and use it from a frontend written with jQuery.
- You can build a GraphQL API for a Rails backend using MySQL as the database and build the frontend with React.
- You can use a CMS like Strapi through a REST API from a React frontend
That's kind of the point of an API: to allow different technologies on both sides of the API.
1.3 API for many different clients
Using an API you can build a common backend for different cleints:
- one client could be a native mobile app for iOS
- another client could be a natvie mobile app for Android
- a third client could be a Single Page App written with React
1.4 Backend offers more than an API
Your "backend" might still offer some "Server Rendered HTML" besides the API:
- Documentation for the API (e.g. swagger, see below)
- AdminPanel (e.g. created automatically by rails_admin)
2 REST
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
- “Hypermedia as the engine of application state” (HATEOAS) - use URLs to reference other resources (not just ids)
- Use HTTP Methods (and Status Codes) as intended.
- One resource can have multiple representations, for example HTML, JSON and XML
- Communicate statelessly - if possible!
2.1 URLS
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()
.
2.2 HATEOAS
“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"
]
}
2.3 HTTP Methods and Status Codes
Use HTTP Methods (and Status Codes) as intended.
Regarding the HTTP Methods there are two important distinctions:
- the GET and HEAD methods should take no other action than retrieval. These methods ought to be considered safe.
- The methods GET, HEAD, PUT and DELETE are idempotent: repeating the request will not change the end result (aside from error or expiration issues)
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.
2.4 Multiple Representations
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"}]}
2.5 Statelessness
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.
2.6 JSON API
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.
2.7 Exploring a REST API
You can explor a REST with several tool:
- curl is a command line tool for sending HTTP requests
- developer tools in the browser can edit "re-send" a request, or copy as curl
- hoppscotch is a browser based REST client
- insomnia is a client you install as a native app
2.8 Testing and Documenting a REST API
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
3 Rails: REST API in with existing Controllers
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.
3.1 API for the sample app
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"
}
]
3.2 creating an API with existing controllers
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.
3.3 creating JSON with erb - not a good idea
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.
3.4 creating JSON with jbuilder - a better idea
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
3.5 Authentication and the API
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.
4 Rails: Stand Alone REST API
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!
4.1 setting up controllers
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.
4.2 creating JSON with blueprinter
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
5 See Also
- Fielding, Roy(2000): Architectural Styles and the Design of Network-based Software Architectures. Dissertation. University of California/Irvine, USA.
- Fowler (2010): Richardson Maturity Model
- Tilkov(2007): A Brief Introduction to REST
- standards.rest a collection of standards that have developed around REST
- Rails Guide: Rendering JSON in Action Controller Overview
- Rails Guide: Using Rails for API-only Applications
- Methods HTTP/1.0
- Status codes
- gem knock for token based authentication for API only Rails apps
- API Platform to build REST + GraphQL APIs in PHP
- Halliday(2016): Producing Documentation for Your Rails API for a discussion of automatic methods of documentation generation.