What does it mean for an API to be RESTful?

The most asked question with the most ambiguous answer(s). "What does it mean for a web API to be RESTful?".

In short, as we saw in the previous chapter, because HTTP has fulfilled almost every requirement of the REST constraints, the only thing missing is the HATEOAS requirement.

A REST API should be entered with no prior knowledge beyond the initial URI (bookmark) and set of standardized media types that are appropriate for the intended audience (i.e., expected to be understood by any client that might use the API). From that point on, all application state transitions must be driven by client selection of server-provided choices that are present in the received representations or implied by the user’s manipulation of those representations. The transitions may be determined (or limited by) the client’s knowledge of media types and resource communication mechanisms, both of which may be improved on-the-fly (e.g., code-on-demand). [Failure here implies that out-of-band information is driving interaction instead of hypertext.]

-Roy Fielding

To put simply, every response returned from the server to the client must provide appropriate links to the other resources in the application.

A Not-So-RESTful Case Study: PivotalTracker

I am getting frustrated by the number of people calling any HTTP-based interface a REST API.

-Roy Fielding

Let's examine a "REST API". For those we are not familiar with PivotalTracker, it's essential a project management tracking tool similar to Trello and Jira. When I first used this API, I thought to myself, where did the word "REST" come from in terms of its title on the documentation page?

I dug a little deeper to just make sure, and have confirmed that this API is truly by no means RESTful. Let's explore why is not a REST API.

The official documentation can be found here: https://www.pivotaltracker.com/help/api/rest/v5.

First, let's take a look at the GET /projects/{project_id} endpoint.

The response:

{
  "account_id": 100,
  "atom_enabled": false,
  "automatic_planning": true,
  "bugs_and_chores_are_estimatable": false,
  "created_at": "2021-01-12T12:00:05Z",
  "current_iteration_number": 1,
  "enable_following": true,
  "enable_incoming_emails": true,
  "enable_tasks": true,
  "has_google_domain": false,
  "id": 1900,
  "initial_velocity": 10,
  "iteration_length": 1,
  "kind": "project",
  "name": "Executioner",
  "number_of_done_iterations_to_show": 4,
  "point_scale": "0,1,2,3",
  "point_scale_is_custom": false,
  "project_type": "private",
  "public": false,
  "start_time": "2021-01-12T12:00:00Z",
  "time_zone": {
    "kind": "time_zone",
    "olson_name": "America/Los_Angeles",
    "offset": "-08:00"
  },
  "updated_at": "2021-01-12T12:00:05Z",
  "velocity_averaged_over": 3,
  "version": 1,
  "week_start_day": "Monday"
}

There is no sign of any links.

What about the POST /projects endpoint?

The response:

{
  "account_id": 100,
  "atom_enabled": true,
  "automatic_planning": true,
  "bugs_and_chores_are_estimatable": false,
  "created_at": "2021-01-12T12:00:05Z",
  "current_iteration_number": 15,
  "description": "Expeditionary Battle Planetoid",
  "enable_following": true,
  "enable_incoming_emails": true,
  "enable_tasks": true,
  "has_google_domain": false,
  "id": 99,
  "initial_velocity": 10,
  "iteration_length": 1,
  "kind": "project",
  "name": "Death Star",
  "number_of_done_iterations_to_show": 4,
  "point_scale": "0,1,2,3",
  "point_scale_is_custom": false,
  "profile_content": "This is a machine of war such as the universe has never known. It's colossal, the size of a class-four moon. And it possesses firepower unequaled in the history of warfare.",
  "project_type": "private",
  "public": false,
  "start_date": "2020-09-28",
  "start_time": "2021-01-12T12:00:10Z",
  "time_zone": {
    "kind": "time_zone",
    "olson_name": "America/Los_Angeles",
    "offset": "-08:00"
  },
  "updated_at": "2021-01-12T12:00:10Z",
  "velocity_averaged_over": 3,
  "version": 66,
  "week_start_day": "Monday"
}

Again, it gives the same general structure as the GET request previously, there are no links what's so ever.

So there you have it, PivotalTracker, although being a very well documented and easy to use API, is indeed not RESTful.

A Truly RESTful Case Study: Twilio

But hold on there... let's not end this section on a bad note, because the truth is that there are plenty of good public APIs that are truly RESTful. One of the more well known APIs (by developers) is the Twilio API. I got to say, Twilio has done a great job documenting all of their APIs and have definitely been able to adhere (almost 100%) to the hypermedia constraints by providing as many links within their responses as possible.

The official documentation that we are going to be exploring can be found here: https://www.twilio.com/docs/usage/api

Let's begin by first examining the POST https://api.twilio.com/2010-04-01/Accounts.json create account endpoint.

The response:

{
  "auth_token": "auth_token",
  "date_created": "Thu, 30 Jul 2015 20:00:00 +0000",
  "date_updated": "Thu, 30 Jul 2015 20:00:00 +0000",
  "friendly_name": "friendly_name",
  "owner_account_sid": "ACX",
  "sid": "ACX",
  "status": "active",
  "subresource_uris": {
    "available_phone_numbers": "/2010-04-01/Accounts/ACX/AvailablePhoneNumbers.json",
    "calls": "/2010-04-01/Accounts/ACX/Calls.json",
    "conferences": "/2010-04-01/Accounts/ACX/Conferences.json",
    "incoming_phone_numbers": "/2010-04-01/Accounts/ACX/IncomingPhoneNumbers.json",
    "notifications": "/2010-04-01/Accounts/ACX/Notifications.json",
    "outgoing_caller_ids": "/2010-04-01/Accounts/ACX/OutgoingCallerIds.json",
    "recordings": "/2010-04-01/Accounts/ACX/Recordings.json",
    "transcriptions": "/2010-04-01/Accounts/ACX/Transcriptions.json",
    "addresses": "/2010-04-01/Accounts/ACX/Addresses.json",
    "signing_keys": "/2010-04-01/Accounts/ACX/SigningKeys.json",
    "connect_apps": "/2010-04-01/Accounts/ACX/ConnectApps.json",
    "sip": "/2010-04-01/Accounts/ACX/SIP.json",
    "authorized_connect_apps": "/2010-04-01/Accounts/ACX/AuthorizedConnectApps.json",
    "usage": "/2010-04-01/Accounts/ACX/Usage.json",
    "keys": "/2010-04-01/Accounts/ACX/Keys.json",
    "applications": "/2010-04-01/Accounts/ACX/Applications.json",
    "short_codes": "/2010-04-01/Accounts/ACX/SMS/ShortCodes.json",
    "queues": "/2010-04-01/Accounts/ACX/Queues.json",
    "messages": "/2010-04-01/Accounts/ACX/Messages.json",
    "balance": "/2010-04-01/Accounts/ACX/Balance.json"
  },
  "type": "Full",
  "uri": "/2010-04-01/Accounts/ACX.json"
}

As you can see not only does it contain the "uri" property of the account that has just been created, it also provides a bunch of "subresource_uris".

Next, let's checkout the GET https://api.twilio.com/2010-04-01/Accounts/ACXXXXX.json fetch account endpoint.

The response:

{
  "auth_token": "auth_token",
  "date_created": "Thu, 30 Jul 2015 20:00:00 +0000",
  "date_updated": "Thu, 30 Jul 2015 20:00:00 +0000",
  "friendly_name": "friendly_name",
  "owner_account_sid": "ACX",
  "sid": "ACX",
  "status": "active",
  "subresource_uris": {
    "available_phone_numbers": "/2010-04-01/Accounts/ACX/AvailablePhoneNumbers.json",
    "calls": "/2010-04-01/Accounts/ACX/Calls.json",
    "conferences": "/2010-04-01/Accounts/ACX/Conferences.json",
    "incoming_phone_numbers": "/2010-04-01/Accounts/ACX/IncomingPhoneNumbers.json",
    "notifications": "/2010-04-01/Accounts/ACX/Notifications.json",
    "outgoing_caller_ids": "/2010-04-01/Accounts/ACX/OutgoingCallerIds.json",
    "recordings": "/2010-04-01/Accounts/ACX/Recordings.json",
    "transcriptions": "/2010-04-01/Accounts/ACX/Transcriptions.json",
    "addresses": "/2010-04-01/Accounts/ACX/Addresses.json",
    "signing_keys": "/2010-04-01/Accounts/ACX/SigningKeys.json",
    "connect_apps": "/2010-04-01/Accounts/ACX/ConnectApps.json",
    "sip": "/2010-04-01/Accounts/ACX/SIP.json",
    "authorized_connect_apps": "/2010-04-01/Accounts/ACX/AuthorizedConnectApps.json",
    "usage": "/2010-04-01/Accounts/ACX/Usage.json",
    "keys": "/2010-04-01/Accounts/ACX/Keys.json",
    "applications": "/2010-04-01/Accounts/ACX/Applications.json",
    "short_codes": "/2010-04-01/Accounts/ACX/SMS/ShortCodes.json",
    "queues": "/2010-04-01/Accounts/ACX/Queues.json",
    "messages": "/2010-04-01/Accounts/ACX/Messages.json",
    "balance": "/2010-04-01/Accounts/ACX/Balance.json"
  },
  "type": "Full",
  "uri": "/2010-04-01/Accounts/ACX.json"
}

Again, same general format.

This is great because it gives you everything you need. Imagine as a consumer of this API when I make a fetch to a specified account ID, I can more or less access all necessary endpoints of that particular user without ever consulting the documentation.

Do I need to know what the usage for that particular account will look like? We have the information right here "usage": "/2010-04-01/Accounts/ACXX/Usage.json".

What about all the calls or notifications we want to see for that particular account? They are all under the "subresource_uris" property of this response.

Last updated