The API Design

Sergey Konstantinov
5 min readDec 13, 2020

This year, everyone finds their own way to pass the time. I am writing the book, for example. The book on APIs.

Short intro: I’ve been working with APIs for over a decade: developing maps APIs, integration APIs, reviewing W3C specs (see my Linkedin profile for details).

I’ve finished the first section of the book, dedicated exclusively to the API design. You may read it online here: https://twirl.github.io/The-API-Book/docs/API.en.html. There are also .pdf and .epub versions, you may find them on Github. The book is distributed under a CC-BY-BC license, meaning it’s available for free.

To arouse your interest — a portion of Chapter 11:

1. Explicit is always better than implicit

Entity’s name must explicitly tell what it does and what side effects to expect while using it.

Bad:

// Cancels an order
GET /orders/cancellation

It’s quite a surprise that accessing the cancellation resource (what is it?) with non-modifying GET method actually cancels an order.

Better:

// Cancels an order
POST /orders/cancel

Bad:

// Returns aggregated statistics
// since the beginning of time
GET /orders/statistics

Even if the operation is non-modifying, but computationally expensive, you should explicitly indicate that, especially if clients got charged for computational resource usage. Even more so, default values must not be set in a manner leading to maximum resource consumption.

Better:

// Returns aggregated statistics
// for a specified period of time
POST /v1/orders/statistics/aggregate
{ "begin_date", "end_date" }

Try to design function signatures to be absolutely transparent about what the function does, what arguments takes and what’s the result. While reading a code working with your API, it must be easy to understand what it does without reading docs.

Two important implications:

1.1. If the operation is modifying, it must be obvious from the signature. In particular, there might be no modifying operations using GET verb.

1.2. If your API’s nomenclature contains both synchronous and asynchronous operations, then (a)synchronicity must be apparent from signatures, or a naming convention must exist.

2. Specify which standards are used

Regretfully, the humanity is unable to agree on the most trivial things, like which day starts the week, to say nothing about more sophisticated standards.

So always specify exactly which standard is applied. Exceptions are possible, if you 100% sure that only one standard for this entity exists in the world, and every person on Earth is totally aware of it.

Bad: "date": "11/12/2020" — there are tons of date formatting standards; you can't even tell which number means the day number and which number means the month.

Better: "iso_date": "2020-11-12".

Bad: "duration": 5000 — five thousands of what?

Better:
"duration_ms": 5000
or
"duration": "5000ms"
or
"duration": {"unit": "ms", "value": 5000}.

One particular implication from this rule is that money sums must always be accompanied with currency code.

It is also worth saying that in some areas the situation with standards is so spoiled that, whatever you do, someone got upset. A ‘classical’ example is geographical coordinates order (latitude-longitude vs longitude-latitude). Alas, the only working method of fighting with frustration there is a ‘serenity notepad’ to be discussed in Section II.

3. Keep fractional numbers precision intact

If the protocol allows, fractional numbers with fixed precision (like money sums) must be represented as a specially designed type like Decimal or its equivalent.

If there is no Decimal type in the protocol (for instance, JSON doesn’t have one), you should either use integers (e.g. apply a fixed multiplicator) or strings.

4. Entities must have concrete names

Avoid single amoeba-like words, such as get, apply, make.

Bad: user.get() — hard to guess what is actually returned.

Better: user.get_id().

5. Don’t spare the letters

In XXI century there’s no need to shorten entities’ names.

Bad: order.time() — unclear, what time is actually returned: order creation time, order preparation time, order waiting time?…

Better: order.get_estimated_delivery_time()

Bad:

// Returns a pointer to the first occurrence
// in str1 of any of the characters
// that are part of str2
strpbrk (str1, str2)

Possibly, an author of this API thought that pbrk abbreviature would mean something to readers; clearly mistaken. Also it's hard to tell from the signature which string (str1 or str2) stands for a character set.

Better: str_search_for_characters (lookup_character_set, str)
— though it's highly disputable whether this function should exist at all; a feature-rich search function would be much more convenient. Also, shortening string to str bears no practical sense, regretfully being a routine in many subject areas.

6. Naming implies typing

Field named recipe must be of Recipe type. Field named recipe_id must contain a recipe identifier which we could find within Recipe entity.

Same for primitive types. Arrays must be named in a plural form or as collective nouns, i.e. objects, children. If that's impossible, better add a prefix or a postfix to avoid doubt.

Bad: GET /news — unclear whether a specific news item is returned, or a list of them.

Better: GET /news-list.

Similarly, if a Boolean value is expected, entity naming must describe some qualitative state, i.e. is_ready, open_now.

Bad: "task.status": true
— statuses are not explicitly binary; also such API isn't extendable.

Better: "task.is_finished": true.

Specific platforms imply specific additions to this rule with regard to first class citizen types they provide. For examples, entities of Date type (if such type is present) would benefit from being indicated with _at or _date postfix, i.e. created_at, occurred_at.

If entity name is a polysemantic term itself, which could confuse developers, better add an extra prefix or postfix to avoid misunderstanding.

Bad:

// Returns a list of coffee machine builtin functions
GET /coffee-machines/{id}/functions

Word ‘function’ is many-valued. It could mean builtin functions, but also ‘a piece of code’, or a state (machine is functioning).

Better: GET /v1/coffee-machines/{id}/builtin-functions-list

7. Matching entities must have matching names and behave alike

Bad: begin_transition / stop_transition
begin and stop doesn't match; developers will have to dig into the docs.

Better: either begin_transition / end_transition or start_transition / stop_transition.

Bad:

// Find the position of the first occurrence
// of a substring in a string
strpos(haystack, needle)
// Replace all occurrences
// of the search string with the replacement string
str_replace(needle, replace, haystack)

Several rules are violated:

  • inconsistent underscore using;
  • functionally close methods have different needle/haystack argument order;
  • first function finds the first occurrence while second one finds them all, and there is no way to deduce that fact out of the function signatures.

We’re leaving the exercise of making these signatures better to the reader.

--

--