This is the second blog post on building hypermedia APIs with the focus on API design. In part 1 Lucas describes the concept of links in JSON.
Imagine we have an API where people can like books and other people can then see, who likes a certain book. We want this API to be highly connected: We don’t want to look up URLs in a documentation, we want to follow links as we know it from the world wide web. All we want to do as the author of the API is give our users a single URL from which they can then follow links to all other resources. This is similar to the way we would do this with a website. Leonard Richardson and Mike Amundsen refer to this as the billboard URL for this reason: If you put this URL on some billboard, people know everything to get started with your API. So in our case, we probably want people to get to two places from our billboard: To the
list of all books and to a list of all users. Our potential user will arrive at the billboard URL and can now follow one of those links. Let’s assume we want to get an overview of all the books. We will follow the link to the books and get back a representation of all books.
What if we want to add books to this list? In HTML we would offer a form that would ask for the necessary information to create this book. In a format like Siren we have this possibility as well, so we will add a form to create new books to this endpoint. We also want to get from each entry in this long list to a detail view of this book. In this detail view we will then get all information about this book plus links to all the users that liked this book. We could also add a link to a resource that gives us a count of likes for that book.
This is a long description. What if we want to visualize this? Let’s draw it in a similar way that we would draw a sitemap where those sites that have the same structure, but are differentiable (like the detail pages of books) are marked by being a tiny stack:
Now, let’s add some meaning to those arrows by adding a short description to them:
A statechart emerges
If we close one eye and look at it sideways, doesn’t this kind of look like a statemachine? Our different URLs are the states, our links are the transitions. Clicking a link is following a transition. We however need some extensions to the usual model of statemachines (Those were proposed in 1987 by Harel in his paper about statecharts):
- Parameterized States: As described above, we need to be able to express that certain states are of the same structure, but differentiable by some parameter. In our statechart, we refer to this as parameterized states.
- Conditions: We can define that a certain transition can only be followed when a certain condition is fulfilled.
- Superstates: We furthermore use the notion of superstates to express that a state is contained in another state. This is especially important in the case of having a parameterized state as the superstate as we may want to know which of the parameterized states has been selected and access this specific state.
One thing that’s different from the way that a statechart works is that our statechart can change at runtime:
- Parameterized states can be added and removed. Non-parameterized states can not be added or removed at runtime.
- Transitions might be added or removed at runtime. In our example, we might want to express that two ideas are now related. This requires us to add a transition between those two parameterized states. We don’t want the user of our API to be able to add transitions anywhere they want, so we need to be able to define what kind of transitions we can add where.
We already described how we can define that a state can be added at a certain point in the statechart by using parameterized states. How do we handle adding and removing transitions? Let’s define three different kinds of transitions:
followtransition — this is the normal transition that is defined when creating the statechart and can not be changed at runtime.
connecttransition creates a transition, when you follow it. In the case of our related ideas (follow) transition, we can create an additional transition that can add transitions of this kind.
disconnecttransition removes a transition that has been created by a
What else should a transition be able to do?
to: Does our transition lead to one state or two different ones? We only need to ask this question when our target is a parameterized state (otherwise the answer is always one). In the case of the parameterized state this is for example the difference of being able to add only one related idea or multiple ones.
modify: Our transitions don’t have side effects. So we introduce a fourth kind of transition which always leads from one state with data to itself, which allows us to modify the data in that state.
parameters: Some transitions need to have certain parameters. If our transition creates a new idea for example, we need to know the title and content of this idea.
condition: As mentioned above, some of our transitions should only be followable if a certain condition is fulfilled. This can for example be used for authorization.
Now that we have a more sophisticated way to describe our transitions, let’s try to describe our states:
- The books transition that leads us from the start state to our books state is a
followtransition that doesn’t need any additional information. The same is true for our users transition.
- The two item transitions that lead us from a collection to a specific entry of that
collectionare also follow transition.
- The createBook transition is a
connecttransition that takes some parameters to create a specific book.
- The likes transition to get to the list of likes is also a simple
- Then we need three transitions between books and users to
disconnectbooks and users. We want those to be
tomany, because we want a book to be liked by more than one person.
With the defined transitions, we now take another look at our states. In our design approach we have three different kinds of states. To understand the difference we first need to do a small excursion.
Entities, Value Objects and Aggregates
When we look at our data, there’s some data that has its own identity. An example for that is a person: If we change data about a person (like the person’s name), it is still the same person. This is not true for all kinds of data: In most systems, a street doesn’t have its own identity — it is only identified by its values. That means that if we change something about that street, it will be a different street. In consequence, we can treat it as being immutable and copy it as many times as we want. If our data has an entity, we call it an entity — otherwise we refer to it as a value object. When using entities and value objects in a multi model database like ArangoDB, we can treat them differently:
- If we want to connect an entity with one or more value objects, we can embed the value objects in the entity and store it as a single document. This is often referred to as an aggregate, but we can simple call it an entity. In our API we can also embed them as we are using JSON.
- If we want to connect multiple entities, we connect them with links in our API and edges in our storage.
In our API we have two kinds of entities:
Repositories and Services
To store those entities, we need another concept called the repository. A repository is an object that is a list of entities which is backed by a database in most cases. We want to add and remove entries to and from the repository. Furthermore we want to search through the entries.
Furthermore there are objects that just do something. They might use a repository or entity to do their job, but they are stateless themselves. We refer to those objects as services.
Describing the states
With those three kinds of objects, we now have the vocabulary to describe our states:
usersare both repositories
userare both parameterized entities
likesis a service
For repositories we only need the information which entities they should store. For entities we need to provide information on which repository stores them and what attributes they have. And our service needs information about what it should do — in this case counting the likes of a certain book.
With that we now have designed our entire API. Notice that we didn’t talk about URLs, status codes or how to structure requests and responses. We will see why in the third and final post in this series.