REST APIs

After working through this guide you will:

You can study the code and try out the demo for the example described here.


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:

  1. Give every resource a unique URL
  2. “Hypermedia as the engine of application state” (HATEOAS) - use URLs to reference other resources (not just ids)
  3. Use HTTP Methods (and Status Codes) as intended.
  4. One resource can have multiple representations, for example HTML, JSON and XML
  5. 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