04.09.13 | User-Driven API Design

We recently rolled out v1 of the Sendicate API. An important goal was to optimize the API for readability and understanding. This is important considering engineers spend ten times more time reading code than writing code.

Essentially, an API is a product for engineers. How it will be used is something only real users can tell us, so the input from our beta users was invaluable. Having immediate feedback every step of the way enabled us to build a practical API.

We also learned a lot from becoming users of the API ourselves. We created a Sendicate API Ruby gem that we use to subscribe new users to our mailing list.

Here are a few lessons learned along the way.

UI Should Not Influence the API

A feature common in email applications is subscriber lists. A subscriber list is used to represent a category of subscribers such as beta testers, employees, customers, etc. A subscriber list often has its own subscribe form as well. We may not want to gather as much information from customers as we do from our employees.

With similar APIs, you'll often see each list has its own set of fields. However, why should it be a concern of the API which fields are relevant to which lists? This is only a concern of the subscribe form. Should a list be constrained to the appearance of a form?

Originally our plan was to provide a custom set of fields for each list. However, we realized there was no need to do this. Which fields are displayed on a list's subscribe form is the concern of the app that is consuming the API, not a concern of the API itself.

Denormalize Data to Improve Readability

Sendicate allows users to define custom fields that provide additional information about their subscribers. For instance, a user could create a custom field called "age" to store the age of all their subscribers.

It is common to see custom fields represented as so:

{
  "email": "user@example.com",
  "name": "subscriber",
  "custom_fields": [
    {
      "key": "age",
      "value": "24"
    }
  ]
}

Notice how custom fields are represented differently than default fields. This is likely tied to how the data is stored in the database. The schema that is stored in the database should not prevent us from simplifying the format of the data as represented by the API.

{
  "email": "user@example.com",
  "name": "subscriber",
  "age": 24
}

This denormalized data is easier to read. The important data is the data describing the subscriber. There is no extra information describing how the data is stored.

Simplifying REST

We provide an API endpoint to allow users to import one or many subscribers into a list. If an imported subscriber exists, then that subscriber should be updated with the new information.

With a typical RESTful API, the endpoint for creating a resource responds to the HTTP POST method and the endpoint for updating a resource responds to the HTTP PUT method.

For our specific use case, we actually need a hybrid endpoint that can both create and update subscribers. We opted to use the POST method for both creating and updating subscribers.

There is also a lot of debate about how to handle batch operations. In this specific case, it is possible to accept either one subscriber or an array of subscribers at this endpoint. The business logic for both operations is the same, so there is no need for a separate endpoint for batch operations.

Using Your Head...ers

Often you'll see APIs use the URI to provide extra information about the resource being requested. You may see the content type specified with a file extension or the authentication token provided with a query string parameter.

GET https://api.sendicate.net/v1/lists.json?token=API_TOKEN

This may not seem a big deal. However, while building our API gem it became cumbersome to append this extra information to every request. We used HTTP header fields to provide this information, instead. This greatly simplified the code for the API wrapper and produced a URI that is much easier to read.

GET https://api.sendicate.net/v1/lists

Accept: application/json
Authorization: token YOUR_API_TOKEN
Content-Type: application/json

Try it for Yourself

To get started using the Sendicate API head over to our API documentation on Github, or grab the Sendicate ruby gem.