Skip to content

Instantly share code, notes, and snippets.

@nrktkt
Last active December 21, 2021 11:50
Show Gist options
  • Save nrktkt/8b904369eae0bdb14541 to your computer and use it in GitHub Desktop.
Save nrktkt/8b904369eae0bdb14541 to your computer and use it in GitHub Desktop.
Mastering REST

Master REST: Mastering HTTP REST With JSON and HAL

Intro

This guide will go over the process to create a REST API at the highest level of mastery. It is worth noting that while a REST API can work over any data exchange protocol (not just HTTP), and with any data serialization format (not just JSON), these are still the most common ways to implement REST. We will use the Richardson Maturity Model as a primary gauge of our success, although we will address patterns and design issues not considered there.

Prerequisite knowledge

  • General goals of REST
  • HTTP protocol
  • JSON

Planning the data model

Before we start the API, it is useful to know the model of the data we will be working with. For this we will use an entity-relationship like diagram to describe, well, the types of entities and their relationships.

A few examples:

Two entities, ParentEnt has zero or more "child" ChildEnts

Two entities, same as above but ChildEnt also has exactly one "parent" ParentEnt

Notes on quantifiers:
  • * zero or more
  • n exactly n
  • + one or more
  • ? zero or one

For this guide we will describe a pet store. The pet store API will have pets, people, appointments and doctors. Our data model will be as follows:

Some examples of serialized models would be: a pet:

{
  "name": "fido",
  "type": "dog",
  "id": 5
}

a person:

{
  "name": "John Doe",
  "phone": "619-867-5309",
  "id": 2
}

etc.
You get the idea

Resource paths (Level 1)

To reach a level 1 REST API, we need to establish distinct resources. To do this we have two types of resources, collections and elements. Each resource has a location described by a URI path.

Collections

Collections are, intuitively, collections of elements. Typical paths for a collection might look like /users, /items, or /cities. Collections are named in the plural (items instead of item).

Elements

Elements are almost always a member of a collection. Elements can be represented as a map, or a list. Most commonly, an element is implemented as a map from a key to an element. Typical paths for elements might look like /users/jim, /items/myBox, or /cities/SanFrancisco.

It is also possible to combine collections and elements several levels deep. For example: /cities/SanFrancisco/restaurants/goldenBoy.

Our API so far

With this in mind, our pet store API will have the following resource paths:

/pets
/pets/{id}
/appointments
/appointments/{id}
/people
/people/{id}
/doctors
/doctors/{id}

Our elements here are identified by system generated ids (likely a nonce), however this does not always need to be the case. Any unique immutable property can be used. For example, in a user system where users have unique usernames and usernames cannot be changed, /users/jdoe123 would be a good way to identify a user element.

Verbs (Level 2)

To bring our API to the next level, we need to be able to manipulate the resources we have created. Typically most resource manipulation is done to the elements.

Create

When we want to create an element, we use the HTTP POST method on a collection, passing a serialized representation of the element we want to create in the body. For example, the request for creation of a pet in our API would look like this:

POST /pets http/1.1

{"name":"fido","type": "dog"}

and the response would look like this:

HTTP/1.1 201 Created
Location: /pets/5

Note that there was no id in the request object even though id is a field of a pet element. Sometimes fields are created by the server rather than the client. These fields do not need to be, and should not be sent in the creation or update of an object and a server should ignore or reject them if they are present.

Read

Reading resources is one of the easiest operations. Both collections and elements can be read with the GET method.
Reading a collection might look like this:

GET /pets http/1.1
HTTP/1.1 200 OK

[{
    "name": "Princess Carolyn","type": "cat","id": 1
  },{
    "name": "fido","type": "dog","id": 5
}]

Reading an element might look like this:

GET /pets/5 http/1.1
HTTP/1.1 200 OK

{"name": "fido","type": "dog","id": 5}

Update

Typically only elements are updated, in most cases if you want to modify a collection it is done through creation or destruction of an element in that collection.
There are two ways to update an element, replacing it in place using the PUT method, or updating some of its fields with the PATCH method.

PUT

Replacing an element in place means sending a full representation of the element. Any fields present in the representation will be changed, and any fields absent will be removed (or set to null). Like creation of an object, the representation should not include system controlled fields like id. Changing the name of a pet with PUT for our API might look like this:

PUT /pets/5 http/1.1

{"name":"spike","type": "dog"}
HTTP/1.1 204 No Content

PATCH

Sometimes the client doesn't have all the fields of an element available to create a full representation for a PUT, or a full representation is large and heavy on network resources. In these and many other cases the PATCH method can be used to update only selected fields. While there exist complex ways to modify a resource (like JSON Patch) we prefer simpler and more straightforward ways of patching. To patch a resource a client simply sends only the fields and new values which are desired. Changing the name of a pet with PATCH for our API might look like this:

PATCH /pets/5 http/1.1

{"name":"spike"}
HTTP/1.1 204 No Content

Unlike POST, this would not try to remove or null the type field.

Destroy

One of the easiest operations, destruction of a resource is typically allowed for elements and not for collections. Elements are destroyed using the DELETE method. A straightforward example for deleting a pet in our API is below:

DELETE /pets/5 http/1.1

HTTP/1.1 204 No Content

Our API so far

POST	/pets
GET		/pets
GET 	/pets/{id}
PATCH	/pets/{id}
DELETE	/pets/{id}

GET		/appointments
POST	/appointments
GET		/appointments/{id}
PATCH	/appointments/{id}
DELETE	/appointments/{id}

GET		/people
POST	/people
GET		/people/{id}
PATCH	/people/{id}
DELETE	/people/{id}

GET		/doctors
POST	/doctors
GET		/doctors/{id}
PATCH	/doctors/{id}
DELETE	/doctors/{id}

HATEOAS (Level 3)

To get our API to a master level, we need to address the HATEOAS property of REST. HATEOAS for us primarily means that by using part of the API, the client becomes aware of other parts of the API. For our pet store this means that clients getting a list of pets become aware of how to interact with individual pets, and when interacting with individual pets the client becomes aware of the pet's owner and appointments.

HAL

To accomplish these goals, we will use the HAL specification. HAL will help us describe the relationships between different resources. We won't go into detail about the HAL format since the spec describes it, rather we will talk about how it is used.

Relationships

Representation

In our relationship diagram, we drew directed edges between entities, but our API up to this point didn't display those relationships. On this level, we now will. HAL has two ways to show resources to which a given resource has a relationship.

Elements

First, there are links. Links simply tell what a relationship is, and where the target of the relationship can be found. The simplest link that all resources have is a self link. A person element with only a self link would look like this:

{	"_links": {
		"self": {"href": "/people/2"}
	},
	"name": "John Doe",
	"phone": "619-867-5309" }

Note that the id field is gone, it is no longer needed since it is only used to build the path to the element and we now have that provided in the self link.

Links to other resources are provided in the same way as the self link. A person with a preferred doctor and one pet would look like this:

{	"_links": {
		"self": {"href": "/people/2"},
		"pets": [{"href": "/pets/5"}],
		"preferredDoctor": {"href": "/doctors/1"}
	},
	"name": "John Doe",
	"phone": "619-867-5309" 
}

Note that even though this person only has one pet, their pets link is an array. This is because we determined that a person can have more than one pet and we want our schema to be uniform across all people.

The second type of relationship we can do in HAL is an embedded resource. With a person it makes sense for their relationships to be links, the other resources aren't critical to the person. However for something like an appointment element, the appointment isn't too useful without the details of the resources it is related to. For this reason, embedded resources have the entire resource with them. For example:

{ "_links": {"self": {"href": "/appointments/7"}},
  "_embedded": {
    "attending": {
      "_links": {
        "self": {"href": "/doctors/1"},
        "appointments": [{"href": "/appointments/7"}]
      },
      "name": "Dr. Bob"
    },
    "patient": {
      "_links": {
        "self": {"href": "/pets/5"},
        "owner": {"href": "/people/2"},
        "appointments": [{"href": "/appointments/7"}]
      },
      "name": "fido",
      "type": "dog"
    }
  },
  "time": 1451865265
}

Now we have both the details about the appointment itself, as well as details about the pet and the doctor, without needing to make extra calls or consult any documentation.

Collections

Now that we have gone over representing elements with HAL, we want to know how to represent collections. Fortunately this is very simple. A collection has itself as a link, and it's elements as embedded resources. For example, the collection of people might look like this:

{ "_links": {"self": {"href": "/people"}},
  "_embedded": {
    "people": [
      { "_links": {
          "self": {"href": "/people/2"},
          "pets": [{ "href": "/pets/5" }],
          "preferredDoctor":{"href": "/doctors/1"}
        },
        "name": "John Doe",
        "phone": "619-867-5309"
      },
      { "_links": {
          "self": {"href": "/people/3"},
          "pets": [{"href": "/pets/3"}],
          "preferredDoctor": {"href": "/doctors/1"}
        },
        "name": "Mr. Anderson",
        "phone": "616-867-9999"
      }
    ]
  }
}
Pagination

Sometimes a collection is too big to have all its elements embedded. In this case pagination is used to only return a few elements of the collection in the _embedded object. In addition to embedding fewer elements, the _links object should also be modified to include a link to the next page of items like so:

"_links": {
	"self": { "href": "/people" },
	"next": { "href": "/people?page=2" }
}

Manipulation

Representing relationships in responses is nice and easy. A trickier topic is how to manipulate those resources. For example, what if we want to change a person's preferred doctor? Typically to change an attribute of an element we would do a PUT or PATCH on it, but in this case that would seem rather unwieldy. The solution is to treat relationships as resources. Specifically, resources whose type is actually a link, rather than a representation of an object.
Our example:

PUT /people/2/preferredDoctor http/1.1

/doctors/9
HTTP/1.1 204 No Content

Another approach: sub-resources

The above works nicely to link resources which stand on their own, however sometimes it might make sense to have data which is significant enough to be its own resource, but which cannot exist on its own without another resource. One example might be if we wanted to track a pet's medical history. You might display the medical history as an embedded resource like this

{
  "_links": {...},
  "_embedded": {
    "medicalRecord": [...]
  }
  "name": "fido",
  "type": "dog"
}

and manipulate it at /pets/5/medicalRecords. Instead of elements of this collection as a link type (as above) we would treat them as full medical record objects. This concept of "sub-resources" is distinct from the above "links as resources". They can both exist together in one API, however doing so could be confusing for users of the API. For this reason, you should do one or the other as long as possible.

Representation in requests

"But wait!" you say, "A pet has to have an owner, which means you need a way to specify the owner at the time of the creation of the pet!". You would be correct to say this. This issue gets us into one of the hairiest parts of the API creation.

Our API so far

POST	/pets
GET		/pets
GET 	/pets/{id}
PATCH	/pets/{id}
DELETE	/pets/{id}
PUT		/pets/{id}/owner

GET		/appointments
POST	/appointments
GET		/appointments/{id}
PATCH	/appointments/{id}
DELETE	/appointments/{id}
PUT		/appointments/{id}/patient
PUT		/appointments/{id}/attending

GET		/people
POST	/people
GET		/people/{id}
PATCH	/people/{id}
DELETE	/people/{id}
PUT		/people/{id}/preferredDoctor
DELETE	/people/{id}/preferredDoctor

GET		/doctors
POST	/doctors
GET		/doctors/{id}
PATCH	/doctors/{id}
DELETE	/doctors/{id}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment