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.
- General goals of REST
- HTTP protocol
- JSON
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.
*
zero or moren
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
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 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 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
.
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 id
s (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.
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.
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.
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}
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.
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
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.
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
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}
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.
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.
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.
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.
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"
}
]
}
}
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" }
}
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
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.
"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.
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}