We’ve recently started redesigning Semaphore’s public API, and we’ve established some general guidelines and semantics for elements in the URI path. A good understanding of URI path element semantics can ease API usage, so we are presenting it here.
Our journey started with a few simple questions:
- How should we organize the resources?
- How deep should the URL hierarchy be?
- Should all elements the system operates on be exposed (more-or-less the way they are represented in the DB), or should there be some layer of abstraction? Also, if we opt for the abstraction layer, which ‘abstract’ elements should map to which ‘less abstract’ elements, and how?
The initial idea was to expose all the elements on which the system operates more-or-less in the same way in which they are represented in the database. This is a simple approach. The resources, i.e. the tables in database, are already identified, and the URL hierarchy is reasonably deep. On the other hand, such design burdens users with too many details of internal implementation, and also makes API consumption unnecessarily complex.
It’s common knowledge that appropriate abstractions make an API easier to use. But, what abstraction is general enough to be systematically applied to the entire API? Models in Semaphore could be designed using UML diagrams, so we decided to RESTify the UML class diagrams of Semaphore models.
There are basically two types of entities in a class diagram — objects and relationships.
We decided to explicitly expose only objects as resources. Relationships are managed implicitly, by referencing the objects in them.
Also, we decided to limit URL path depth to 4 elements and provide a semantic for each element. A URL can have one of the following formats:
Each of the formats has a specific semantic.
- Resources with one or two elements in the path represent objects.
- Resources with 1 element in the URL path are collections containing objects of the class type. This form is used to address a group of objects of the same type, e.g.:
- Resources with 2 elements in the URL path are *documents* representing a particular object of the class type, uniquely identified by an :id. This form is used to address a particular object, e.g.:
- Resources with three or four elements in the path represent relationships.
- Resources with 3 elements in the URL path are collections containing objects of the class2 type that are in a relationship with particular object :id1 of the class1 type. For example, to list all the projects in the organization, you should input the following:
- Resources with 4 elements in the URL path are *documents* representing object :id2 of the class2 type, which either is or is going to be after the operation is successfully completed in a relationship with the :id1 object of the class1 type. For example, to add a user named ‘doe’ as a member of some team, you should input the following:
Exposing All Resources vs Exposing Only UML Objects
What would happen if we chose to show all system elements? For example, to add a project to a team resource, the operation would be the same:
However, instead of returning code 204 on success, the call would have to return some kind of an identifier for the relationship established between the team and the project objects.
To remove the same project from the team, the relationship would have to be deleted using the previously mentioned resource identifier, probably something like the following:
We believe that deleting a relationship using the same resource used to create it is a better approach:
We prefer this approach because it’s more descriptive, and the user does not have to handle the relationship identifier.
In our interface model, operations are limited to basic CRUD. Every operation is conducted either on the object(s) themselves, or on the relationships between the objects.
Some operations are conducted on both the object and the relationship. An example of this are POST operations on all resources with 3 elements in the URL (/class1/:id1/class2). All of these operations create objects of the class2 type and establish a relationship between the class2 object and the :id1 object.
creates a new shared configuration within the ‘example-org’ organization, meaning: 1. creates a new shared configuration and 2. establishes a connection between the shared configuration and the ‘example-org’ organization.
Creating an Object
To create an object with object attributes in the request body, use the following format:
Creating a Relationship
To create a relationship between two objects, use the following format:
Requests in this format usually have no body.
Creating Both an Object and a Relationship
To create both an object of the class2 type and a relation between the newly created object and the :id1 object of the class1 type, use this format:
Here, you will need to add object attributes to the request body.
Deleting an object
To delete an object, use the following format:
This operation can either delete relationships referencing the object, or fail.
Deleting a Relationship
To delete a relationship, use the following:
This form is never used to delete the :id2 object.
If either the id1 or the :id2 object cannot exist without the relationship, this operation should not be implemented. These kinds of relationships are deleted when one of the objects is deleted.
Updating an Object
Only one object can be updated with a single operation:
Updating a Relationship
Since relationships don’t have attributes, they cannot be updated. Relationships can only be established (POST), or removed (DELETE).
Listing a Collection of Objects
To list objects of the class type, use the following format:
If the objects are contained, it might be better to avoid implementing this operation and list the objects through a containing object instead, using the relationship with the containing object.
Retrieving a Particular Object
To retrieve a particular object uniquely identified with an :id of the class type, use the following:
There is no way to Retrieve/List relationships as entities, since they are not exposed. Instead, all objects of the class2 type that are in relationship with a particular object id1 of the class1 type are listed with the following:
In addition, there is no Retrieve/List operation on a relationship between two particular objects (/class1/:id1/class2/:id2). Since relationships are not exposed, it would retrieve the details of object :id2. This functionality is implemented with a two-element URI: /class2/id2.
We are still in the limited preview stage of API implementation, but so far it looks like we have chosen an appropriate abstraction that will serve our users well. Follow our future posts below for further updates on the new API.