18 Jul 2017 · Semaphore News

    A First Look at Semaphore’s New API Specification Semantic

    7 min read

    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?

    API Structure

    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.:
      GET /users.
    • 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.:
      GET /users/foo


      DELETE /team/71cf3fd2-be45-4918-84c3-73bfaacd3406
    • 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:
      GET /orgs/example-org/projects
    • 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:
      POST /teams/a67d1794-e658-4c52-9f74-464cf2fffbbb/users/doe

    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:

    POST /teams/f0bbb7e3-1012-4cfb-bd04-f12a97937a4a/projects/56602501-76a1-43c3-85d3-8de6e7335768

    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:

    DELETE /teams/projects/{relationship_identifier}

    We believe that deleting a relationship using the same resource used to create it is a better approach:

    DELETE /teams/f0bbb7e3-1012-4cfb-bd04-f12a97937a4a/projects/56602501-76a1-43c3-85d3-8de6e7335768

    We prefer this approach because it’s more descriptive, and the user does not have to handle the relationship identifier.

    Resource Operations

    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.

    For example:

    POST /orgs/example-org/shared-configurations

    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:

    POST /class

    Creating a Relationship

    To create a relationship between two objects, use the following format:

    POST /class1/:id1/class2/:id2

    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:

    POST /class1/:id1/class2

    Here, you will need to add object attributes to the request body.


    Deleting an object

    To delete an object, use the following format:

    DELETE /class/:id

    This operation can either delete relationships referencing the object, or fail.

    Deleting a Relationship

    To delete a relationship, use the following:

    DELETE /class1/:id1/class2/:id2

    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:

    PATCH /class/:id

    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:

    GET /class

    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:

    GET /class/:id

    Retrieving/Listing Relationships

    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:

    GET /class1/:id1/class2/

    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.

    Wrapping Up

    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.

    Happy building!

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Writen by: