From 3678ea946cbad626127219e30999c2bc4d930c47 Mon Sep 17 00:00:00 2001 From: Sergey Konstantinov Date: Mon, 19 Sep 2022 00:12:52 +0300 Subject: [PATCH] style fix --- docs/API.en.html | 1544 ++++++++++------- .../02-Section I. The API Design/03.md | 50 +- .../02-Section I. The API Design/04.md | 139 +- .../02-Section I. The API Design/05.md | 68 +- .../02-Section I. The API Design/06.md | 39 +- .../02.md | 11 +- .../03.md | 3 +- .../04.md | 13 +- .../05.md | 91 +- 9 files changed, 1171 insertions(+), 787 deletions(-) diff --git a/docs/API.en.html b/docs/API.en.html index fe80e4f..ab76795 100644 --- a/docs/API.en.html +++ b/docs/API.en.html @@ -94,6 +94,7 @@ body { width: 100%; margin: 0; padding: 0; + text-align: justify; } .display-none { @@ -108,13 +109,6 @@ body { text-align: left; } -body, -h6 { - font-family: local-serif, serif; - font-size: 14pt; - text-align: justify; -} - .cc-by-nc-img { display: block; float: left; @@ -135,7 +129,8 @@ pre { } code { - white-space: nowrap; + hyphens: manual; + font-size: 80%; } .img-wrapper img { @@ -153,7 +148,6 @@ pre { box-sizing: border-box; page-break-inside: avoid; overflow-x: auto; - font-size: 80%; } img:not(.cc-by-nc-img) { @@ -188,6 +182,13 @@ h5 { page-break-after: avoid; } +body, +h5, +h6 { + font-family: local-serif, serif; + font-size: 14pt; +} + h6 { font-size: 80%; color: darkgray; @@ -215,11 +216,14 @@ h3 { font-size: 140%; } -h4, -h5 { +h4 { font-size: 120%; } +h5 { + font-size: 110%; +} + .annotation { text-align: justify; } @@ -537,8 +541,9 @@ ul.references li p a.back-anchor { } pre { - margin: 0; + margin: 0.2em 0; padding: 0.2em; + font-size: 80%; } ul, @@ -634,21 +639,23 @@ ul.references li p a.back-anchor {

From our point of view, such a practice cannot be justified. Don't imply hidden taxes on your customers. If you're able to avoid breaking backwards compatibility — never break it.

Of course, maintaining old API versions is a sort of a tax either. Technology changes, and you cannot foresee everything, regardless of how nice your API is initially designed. At some point keeping old API versions results in an inability to provide new functionality and support new platforms, and you will be forced to release a new version. But at least you will be able to explain to your customers why they need to make an effort.

We will discuss API lifecycle and version policies in Section II.

Chapter 5. On Versioning

-

Here and throughout we firmly stick to semver principles of versioning:

+

Here and throughout we firmly stick to semver principles of versioning.

  1. API versions are denoted with three numbers, i.e. 1.2.3.
  2. -
  3. First number (major version) increases when backwards-incompatible changes in the API are shipped.
  4. -
  5. Second number (minor version) increases when new functionality is added to the API, keeping backwards compatibility intact.
  6. -
  7. Third number (patch) increases when a new API version contains bug fixes only.
  8. +
  9. The first number (a major version) increases when backwards-incompatible changes in the API are introduced.
  10. +
  11. The second number (a minor version) increases when new functionality is added to the API, keeping backwards compatibility intact.
  12. +
  13. The third number (a patch) increases when a new API version contains bug fixes only.

Sentences ‘major API version’ and ‘new API version, containing backwards-incompatible changes’ are therefore to be considered equivalent ones.

-

In Section II we will discuss versioning policies in more detail. In Section I, we will just use semver versions designation, specifically v1, v2, etc.

Chapter 6. Terms and Notation Keys

+

It is usually (though not necessary) agreed that the last stable API version might be referenced by either a full version (e.g. 1.2.3) or a reduced one (1.2 or just 1). Some systems support more sophisticated schemes of defining the desired version (for example, ^1.2.3 reads like ‘get the last stable API version that is backwards-compatible to the 1.2.3 version’) or additional shortcuts (for example, 1.2-beta to refer to the last beta-version of the 1.2 API version family). In this book, we will mostly use designations like v1 (v2, v3, etc.) to denote the latest stable release of the 1.x.x version family of an API.

+

The practical meaning of this versioning system and the applicable policies will be discussed in more detail in the ‘Backwards Compatibility Problem Statement’ chapter.

Chapter 6. Terms and Notation Keys

Software development is being characterized, among other things, by the existence of many different engineering paradigms, whose adepts sometimes are quite aggressive towards other paradigms' adepts. While writing this book we are deliberately avoiding using terms like ‘method’, ‘object’, ‘function’, and so on, using the neutral term ‘entity’ instead. ‘Entity’ means some atomic functionality unit, like class, method, object, monad, prototype (underline what you think is right).

As for an entity's components, we regretfully failed to find a proper term, so we will use the words ‘fields’ and ‘methods’.

Most of the examples of APIs will be provided in a form of JSON-over-HTTP endpoints. This is some sort of notation that, as we see it, helps to describe concepts in the most comprehensible manner. A GET /v1/orders endpoint call could easily be replaced with an orders.get() method call, local or remote; JSON could easily be replaced with any other data format. The semantics of statements shouldn't change.

Let's take a look at the following example:

// Method description
-POST /v1/bucket/{id}/some-resource
+POST /v1/bucket/{id}/some-resource⮠
+  /{resource_id}
 X-Idempotency-Token: <idempotency token>
 {
   …
@@ -661,7 +668,10 @@ Cache-Control: no-cache
 {
   /* And this is
      a multiline comment */
-  "error_message"
+  "error_message":
+    "Long error message⮠
+     that will span several⮠
+     lines"
 }
 

It should be read like this:

@@ -672,7 +682,8 @@ Cache-Control: no-cache
  • a specific JSON, containing a some_parameter field and some other unspecified fields (indicated by ellipsis) is being sent as a request body payload;
  • in response (marked with an arrow symbol ) server returns a 404 Not Founds status code; the status might be omitted (treat it like a 200 OK if no status is provided);
  • the response could possibly contain additional notable headers;
  • -
  • the response body is a JSON comprising a single error_message field; field value absence means that field contains exactly what you expect it should contain — some error message in this case.
  • +
  • the response body is a JSON comprising a single error_message field; field value absence means that field contains exactly what you expect it should contain — some error message in this case;
  • +
  • if some token is too long to fit a single line, we will split it into several lines adding to indicate it continues next line.
  • The term ‘client’ here stands for an application being executed on a user's device, either a native or a web one. The terms ‘agent’ and ‘user agent’ are synonymous to ‘client’.

    Some request and response parts might be omitted if they are irrelevant to the topic being discussed.

    @@ -834,7 +845,8 @@ GET /v1/orders/{id} "status": "executing", "operations": [ // description of commands - // being executed on a physical coffee machine + // being executed on + // a physical coffee machine ] } … @@ -861,7 +873,8 @@ GET /programs "type": "lungo" } -
    // Starts an execution of a specified program
    +
    // Starts an execution 
    +// of a specified program
     // and returns execution status
     POST /execute
     {
    @@ -882,7 +895,8 @@ POST /execute
     POST /cancel
     
    // Returns execution status.
    -// The format is the same as in `POST /execute`
    +// The format is the same 
    +// as in the `POST /execute` method
     GET /execution/status
     

    NB. Just in case: this API violates a number of design principles, starting with a lack of versioning; it's described in such a manner because of two reasons: (1) to demonstrate how to design a more convenient API, (2) in the real life, you would really get something like that from vendors, and this API is actually quite a sane one.

    @@ -901,9 +915,13 @@ GET /functions // * pour_water // * discard_cup "type": "set_cup", - // Arguments available to each operation. - // To keep it simple, let's limit these to one: - // * volume — a volume of a cup, coffee, or water + // Arguments available + // to each operation. + // To keep it simple, + // let's limit these to one: + // * volume + // — a volume of a cup, + // coffee, or water "arguments": ["volume"] }, … @@ -915,7 +933,10 @@ GET /functions POST /functions { "type": "set_cup", - "arguments": [{ "name": "volume", "value": "300ml" }] + "arguments": [{ + "name": "volume", + "value": "300ml" + }] }
    // Returns sensors' state
    @@ -999,7 +1020,7 @@ GET /sensors
     

    Please note that knowing the coffee machine API kind isn't required at all; that's why we're making abstractions! We could possibly make interfaces more specific, implementing different run and match endpoints for different coffee machines:

    • POST /v1/program-matcher/{api_type}
    • -
    • POST /v1/programs/{api_type}/{program_id}/run
    • +
    • POST /v1/{api_type}/programs/{id}/run

    This approach has some benefits, like the possibility to provide different sets of parameters, specific to the API kind. But we see no need for such fragmentation. run method handler is capable of extracting all the program metadata and performing one of two actions:

      @@ -1008,7 +1029,11 @@ GET /sensors

    Out of general concerns runtime level for the second-kind API will be private, so we are more or less free in implementing it. The easiest solution would be to develop a virtual state machine that creates a ‘runtime’ (e.g. a stateful execution context) to run a program and control its state.

    POST /v1/runtimes
    -{ "coffee_machine", "program", "parameters" }
    +{ 
    +  "coffee_machine", 
    +  "program", 
    +  "parameters" 
    +}
     →
     { "runtime_id", "state" }
     
    @@ -1035,14 +1060,20 @@ GET /sensors // * "finished" — all operations done "status": "ready_waiting", // Command being currently executed. - // Similar to line numbers in computer programs + // Similar to line numbers + // in computer programs "command_sequence_id", // How the execution concluded: - // * "success" — beverage prepared and taken - // * "terminated" — execution aborted - // * "technical_error" — preparation error - // * "waiting_time_exceeded" — beverage prepared, - // but not taken; timed out then disposed + // * "success" + // — beverage prepared and taken + // * "terminated" + // — execution aborted + // * "technical_error" + // — preparation error + // * "waiting_time_exceeded" + // — beverage prepared, + // but not taken; + // timed out then disposed "resolution": "success", // All variables values, // including sensors state @@ -1210,16 +1241,25 @@ It is important to note that we don't calculate new variables out from sensors d

    If we try writing pseudocode, we will get something like that:

    // Retrieve all possible recipes
    -let recipes = api.getRecipes();
    -// Retrieve a list of all available coffee machines
    -let coffeeMachines = api.getCoffeeMachines();
    +let recipes = 
    +  api.getRecipes();
    +// Retrieve a list of 
    +// all available coffee machines
    +let coffeeMachines = 
    +  api.getCoffeeMachines();
     // Build a spatial index
    -let coffeeMachineRecipesIndex = buildGeoIndex(recipes, coffeeMachines);
    -// Select coffee machines matching user's needs
    -let matchingCoffeeMachines = coffeeMachineRecipesIndex.query(
    -  parameters,
    -  { "sort_by": "distance" }
    -);
    +let coffeeMachineRecipesIndex = 
    +  buildGeoIndex(
    +    recipes, 
    +    coffeeMachines
    +  );
    +// Select coffee machines 
    +// matching user's needs
    +let matchingCoffeeMachines = 
    +  coffeeMachineRecipesIndex.query(
    +    parameters,
    +    { "sort_by": "distance" }
    +  );
     // Finally, show offers to user
     app.display(coffeeMachines);
     
    @@ -1243,7 +1283,12 @@ app.display(coffeeMachines); → { "results": [ - { "coffee_machine", "place", "distance", "offer" } + { + "coffee_machine", + "place", + "distance", + "offer" + } ], "cursor" } @@ -1274,12 +1319,15 @@ app.display(offers);
    {
       "results": [
         {
    -      "coffee_machine", "place", "distance",
    +      "coffee_machine", 
    +      "place", 
    +      "distance",
           "offer": {
             "id",
             "price",
             "currency_code",
    -        // Date and time when the offer expires
    +        // Date and time 
    +        // when the offer expires
             "valid_until"
           }
         }
    @@ -1292,11 +1340,9 @@ app.display(offers);
     

    Error Handling

    And one more step towards making developers' life easier: how an ‘invalid price’ error would look like?

    POST /v1/orders
    -{ … "offer_id" …}
    +{ "offer_id", … }
     → 409 Conflict
    -{
    -  "message": "Invalid price"
    -}
    +{ "message": "Invalid price" }
     

    Formally speaking, this error response is enough: users get the ‘Invalid price’ message, and they have to repeat the order. But from a UX point of view that would be a horrible decision: the user hasn't made any mistakes, and this message isn't helpful at all.

    The main rule of error interfaces in the APIs is: an error response must help a client to understand what to do with this error. All other stuff is unimportant: if the error response was machine-readable, there would be no need for the user-readable message.

    @@ -1318,7 +1364,8 @@ The invalid price error is resolvable: a client could obtain a new price offer a // Error kind "reason": "offer_invalid", "localized_message": - "Something goes wrong. Try restarting the app." + "Something goes wrong.⮠ + Try restarting the app." "details": { // What's wrong exactly? // Which validity checks failed? @@ -1336,40 +1383,37 @@ The invalid price error is resolvable: a client could obtain a new price offer a

    The only possible method of overcoming this law is decomposition. Entities should be grouped under a single designation at every concept level of the API, so developers are never to operate more than 10 entities at a time.

    Let's take a look at a simple example: what the coffee machine search function returns. To ensure an adequate UX of the app, quite bulky datasets are required.

    {
    -  "results": [
    -    {
    -      "coffee_machine_id",
    -      "coffee_machine_type": "drip_coffee_maker",
    -      "coffee_machine_brand",
    -      "place_name": "The Chamomile",
    -      // Coordinates of a place
    -      "place_location_latitude",
    -      "place_location_longitude",
    -      "place_open_now",
    -      "working_hours",
    -      // Walking route parameters
    -      "walking_distance",
    -      "walking_time",
    -      // How to find the place
    -      "place_location_tip",
    -      "offers": [
    -        {
    -          "recipe": "lungo",
    -          "recipe_name": "Our brand new Lungo®™",
    -          "recipe_description",
    -          "volume": "800ml",
    -          "offer_id",
    -          "offer_valid_until",
    -          "localized_price": "Just $19 for a large coffee cup",
    -          "price": "19.00",
    -          "currency_code": "USD",
    -          "estimated_waiting_time": "20s"
    -        },
    -        …
    -      ]
    -    },
    -    …
    -  ]
    +  "results": [{
    +    "coffee_machine_id",
    +    "coffee_machine_type": 
    +      "drip_coffee_maker",
    +    "coffee_machine_brand",
    +    "place_name": "The Chamomile",
    +    // Coordinates of a place
    +    "place_location_latitude",
    +    "place_location_longitude",
    +    "place_open_now",
    +    "working_hours",
    +    // Walking route parameters
    +    "walking_distance",
    +    "walking_time",
    +    // How to find the place
    +    "place_location_tip",
    +    "offers": [{
    +      "recipe": "lungo",
    +      "recipe_name": 
    +        "Our brand new Lungo®™",
    +      "recipe_description",
    +      "volume": "800ml",
    +      "offer_id",
    +      "offer_valid_until",
    +      "localized_price": 
    +        "Just $19 for a large coffee cup",
    +      "price": "19.00",
    +      "currency_code": "USD",
    +      "estimated_waiting_time": "20s"
    +    }, …]
    +  }, …]
     }
     

    This approach is quite normal, alas; could be found in almost every API. As we see, the number of entities' fields exceeds recommended 7, and even 9. Fields are being mixed into one single list, often with similar prefixes.

    @@ -1391,16 +1435,30 @@ The invalid price error is resolvable: a client could obtain a new price offer a // Coffee machine properties "coffee-machine": { "id", "brand", "type" }, // Route data - "route": { "distance", "duration", "location_tip" }, + "route": { + "distance", + "duration", + "location_tip" + }, "offers": [{ // Recipe data - "recipe": { "id", "name", "description" }, + "recipe": { + "id", + "name", + "description" + }, // Recipe specific options - "options": { "volume" }, + "options": + { "volume" }, // Offer metadata - "offer": { "id", "valid_until" }, + "offer": + { "id", "valid_until" }, // Pricing - "pricing": { "currency_code", "price", "localized_price" }, + "pricing": { + "currency_code", + "price", + "localized_price" + }, "estimated_waiting_time" }, …] }, …] @@ -1409,13 +1467,15 @@ The invalid price error is resolvable: a client could obtain a new price offer a

    Such decomposed API is much easier to read than a long sheet of different attributes. Furthermore, it's probably better to group even more entities in advance. For example, a place and a route could be joined in a single location structure, or an offer and a pricing might be combined into some generalized object.

    It is important to say that readability is achieved not only by mere grouping the entities. Decomposing must be performed in such a manner that a developer, while reading the interface, instantly understands: ‘here is the place description of no interest to me right now, no need to traverse deeper’. If the data fields needed to complete some action are scattered all over different composites, the readability doesn't improve but degrades.

    Proper decomposition also helps with extending and evolving the API. We'll discuss the subject in Section II.

    Chapter 11. Describing Final Interfaces

    -

    When all entities, their responsibilities, and relations to each other are defined, we proceed to the development of the API itself. We are to describe the objects, fields, methods, and functions nomenclature in detail. In this chapter, we're giving purely practical advice on making APIs usable and understandable.

    +

    When all entities, their responsibilities, and their relations to each other are defined, we proceed to the development of the API itself. We are to describe the objects, fields, methods, and functions nomenclature in detail. In this chapter, we're giving purely practical advice on making APIs usable and understandable.

    An important assertion at number 0:

    -
    0. Rules are just generalizations
    -

    Rules are not to be applied unconditionally. They are not making thinking redundant. Every rule has a rational reason to exist. If your situation doesn't justify following the rule — then you shouldn't do it.

    +
    0. Rules must not be applied unthinkingly
    +

    Rules are just simply formulated generalizations from one's experience. They are not to be applied unconditionally, and they don't make thinking redundant. Every rule has a rational reason to exist. If your situation doesn't justify following the rule — then you shouldn't do it.

    For example, demanding a specification be consistent exists to help developers spare time on reading docs. If you need developers to read some entity's doc, it is totally rational to make its signature deliberately inconsistent.

    This idea applies to every concept listed below. If you get an unusable, bulky, unobvious API because you follow the rules, it's a motive to revise the rules (or the API).

    It is important to understand that you always can introduce concepts of your own. For example, some frameworks willfully reject paired set_entity / get_entity methods in a favor of a single entity() method, with an optional argument. The crucial part is being systematic in applying the concept. If it's rendered into life, you must apply it to every single API method, or at the very least elaborate a naming rule to discern such polymorphic methods from regular ones.

    +

    Ensuring readability and consistency

    +

    The most important task for the API vendor is to make code written by third-party developers atop of the API easily readable and maintainable. Remember that the law of large numbers works against you: if some concept or a signature might be treated wrong, they will be inevitably treated wrong by a number of partners, and this number will be increasing with the API popularity growth.

    1. Explicit is always better than implicit

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

    Bad:

    @@ -1457,14 +1517,11 @@ or
    "duration": {"unit": "ms", "value": 5000}.

    One particular implication of this rule is that money sums must always be accompanied by a 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 frustration there is the ‘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’.

    +
    3. Entities must have concrete names
    +

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

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

    Better: user.get_id().

    -
    5. Don't spare the letters
    +
    4. Don't spare the letters

    In the 21st 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()

    @@ -1475,9 +1532,15 @@ or
    strpbrk (str1, str2)

    Possibly, an author of this API thought that the 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 a string to an str bears no practical sense, regretfully being a routine in many subject areas.

    -
    6. Naming implies typing
    +

    Better:

    +
    str_search_for_characters(
    +  str,
    +  lookup_character_set
    +)
    +
    +

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

    +

    NB: sometimes field names are shortened or even omitted (e.g., a heterogenous array is passed instead of a set of named fields) to lessen the amount of traffic. In most cases, this is absolutely meaningless as usually the data is compressed at the protocol level.

    +
    5. Naming implies typing

    Field named recipe must be of a Recipe type. Field named recipe_id must contain a recipe identifier that we could find within the 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.

    @@ -1486,15 +1549,16 @@ strpbrk (str1, str2)

    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 the first-class citizen types they provide. For example, entities of the Date type (if such type is present) would benefit from being indicated with _at or _date postfixes, i.e. created_at, occurred_at.

    +

    Specific platforms imply specific additions to this rule with regard to the first-class citizen types they provide. For example, JSON doesn't have a Date object type, so the dates are to be passed as numbers or strings. In this case, it's convenient to mark dates somehow, for example, by adding _at or _date postfixes, i.e. created_at, occurred_at.

    If an 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
    +
    // Returns a list of 
    +// coffee machine builtin functions
     GET /coffee-machines/{id}/functions
     

    Word ‘function’ is many-valued. It could mean built-in 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
    +
    6. Matching entities must have matching names and behave alike

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

    Better: either begin_transition / end_transition or start_transition / stop_transition.

    @@ -1504,7 +1568,8 @@ GET /coffee-machines/{id}/functions strpos(haystack, needle)
    // Replace all occurrences
    -// of the search string with the replacement string
    +// of the search string 
    +// with the replacement string
     str_replace(needle, replace, haystack)
     

    Several rules are violated:

    @@ -1514,13 +1579,263 @@ str_replace(needle, replace, haystack)
  • the first function finds the first occurrence while the 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.

    -
    8. Use globally unique identifiers
    -

    It's considered a good form to use globally unique strings as entity identifiers, either semantic (i.e. "lungo" for beverage types) or random ones (i.e. UUID-4). It might turn out to be extremely useful if you need to merge data from several sources under a single identifier.

    -

    In general, we tend to advise using urn-like identifiers, e.g. urn:order:<uuid> (or just order:<uuid>). That helps a lot in dealing with legacy systems with different identifiers attached to the same entity. Namespaces in urns help to understand quickly which identifier is used and is there a usage mistake.

    -

    One important implication: never use increasing numbers as external identifiers. Apart from the abovementioned reasons, it allows counting how many entities of each type there are in the system. Your competitors will be able to calculate a precise number of orders you have each day, for example.

    -

    NB: in this book, we often use short identifiers like "123" in code examples; that's for reading the book on small screens convenience. Do not replicate this practice in a real-world API.

    -
    9. The system state must be observable by clients
    -

    This rule could be reformulated as ‘don't make clients guess’.

    +
    7. Avoid double negations
    +

    Bad: "dont_call_me": false
    +— humans are bad at perceiving double negation; make mistakes.

    +

    Better: "prohibit_calling": true or "avoid_calling": true
    +— it's easier to read, though you shouldn't deceive yourself. Avoid semantical double negations, even if you've found a ‘negative’ word without a ‘negative’ prefix.

    +

    Also worth mentioning is that making mistakes in the de Morgan's laws usage is even simpler. For example, if you have two flags:

    +
    GET /coffee-machines/{id}/stocks
    +→
    +{
    +  "has_beans": true,
    +  "has_cup": true
    +}
    +
    +

    ‘Coffee might be prepared’ condition would look like has_beans && has_cup — both flags must be true. However, if you provide the negations of both flags:

    +
    {
    +  "beans_absence": false,
    +  "cup_absence": false
    +}
    +
    +

    — then developers will have to evaluate the flag !beans_absence && !cup_absence which is equivalent to !(beans_absence || cup_absence) conditions, and in this transition, people tend to make mistakes. Avoiding double negations helps little, and regretfully only general advice could be given: avoid the situations when developers have to evaluate such flags.

    +
    8. Avoid implicit type conversion
    +

    This advice is opposite to the previous one, ironically. When developing APIs you frequently need to add a new optional field with a non-empty default value. For example:

    +
    POST /v1/orders
    +{}
    +→
    +{ "contactless_delivery": true }
    +
    +

    This new contactless_delivery option isn't required, but its default value is true. A question arises: how developers should discern explicit intention to abolish the option (false) from knowing not it exists (field isn't set). They have to write something like:

    +
    if (Type(
    +      order.contactless_delivery
    +    ) == 'Boolean' &&
    +    order.contactless_delivery == false) { 
    +  …
    +}
    +
    +

    This practice makes the code more complicated, and it's quite easy to make mistakes, which will effectively treat the field in an opposite manner. The same could happen if some special values (i.e. null or -1) to denote value absence are used.

    +

    NB: this observation is not valid if both the platform and the protocol unambiguously support special tokens to reset a field to its default value with zero abstraction overhead. However, full and consistent support of this functionality rarely sees implementation. Arguably, the only example of such an API among those being popular nowadays is SQL: the language has the NULL concept, and default field values functionality, and the support for operations like UPDATE … SET field = DEFAULT (in most dialects). Though working with the protocol is still complicated (for example, in many dialects there is no simple method of getting back those values reset by an UPDATE … DEFAULT query), SQL features working with defaults conveniently enough to use this functionality as is.

    +

    If the protocol does not support resetting to default values as a first-class citizen, the universal rule is to make all new Boolean flags false by default.

    +

    Better

    +
    POST /v1/orders
    +{}
    +→
    +{ "force_contact_delivery": false }
    +
    +

    If a non-Boolean field with specially treated value absence is to be introduced, then introduce two fields.

    +

    Bad:

    +
    // Creates a user
    +POST /v1/users
    +{ … }
    +→
    +// Users are created with a monthly
    +// spending limit set by default
    +{
    +  "spending_monthly_limit_usd": "100",
    +  …
    +}
    +// To cancel the limit null value is used
    +PUT /v1/users/{id}
    +{ 
    +  "spending_monthly_limit_usd": null,
    +  …
    +}
    +
    +

    Better

    +
    POST /v1/users
    +{
    +  // true — user explicitly cancels
    +  //   monthly spending limit
    +  // false — limit isn't canceled
    +  //   (default value)
    +  "abolish_spending_limit": false,
    +  // Non-required field
    +  // Only present if the previous flag
    +  // is set to false
    +  "spending_monthly_limit_usd": "100",
    +  …
    +}
    +
    +

    NB: the contradiction with the previous rule lies in the necessity of introducing ‘negative’ flags (the ‘no limit’ flag), which we had to rename to abolish_spending_limit. Though it's a decent name for a negative flag, its semantics is still unobvious, and developers will have to read the docs. That's the way.

    +
    9. No results is a result
    +

    If a server processed a request correctly and no exceptional situation occurred — there must be no error. Regretfully, an antipattern is widespread — of throwing errors when zero results are found.

    +

    Bad

    +
    POST /v1/coffee-machines/search
    +{
    +  "query": "lungo",
    +  "location": <customer's location>
    +}
    +→ 404 Not Found
    +{
    +  "localized_message":
    +    "No one makes lungo nearby"
    +}
    +
    +

    4xx statuses imply that a client made a mistake. But no mistakes were made by either a customer or a developer: a client cannot know whether the lungo is served in this location beforehand.

    +

    Better:

    +
    POST /v1/coffee-machines/search
    +{
    +  "query": "lungo",
    +  "location": <customer's location>
    +}
    +→ 200 OK
    +{
    +  "results": []
    +}
    +
    +

    This rule might be reduced to: if an array is the result of the operation, then the emptiness of that array is not a mistake, but a correct response. (Of course, if an empty array is acceptable semantically; an empty array of coordinates is a mistake for sure.)

    +
    10. Errors must be informative
    +

    While writing the code developers face problems, many of them quite trivial, like invalid parameter types or some boundary violations. The more convenient are the error responses your API return, the less is the amount of time developers waste struggling with it, and the more comfortable is working with the API.

    +

    Bad:

    +
    POST /v1/coffee-machines/search
    +{
    +  "recipes": ["lngo"],
    +  "position": {
    +    "latitude": 110,
    +    "longitude": 55
    +  }
    +}
    +→ 400 Bad Request
    +{}
    +
    +

    — of course, the mistakes (typo in the "lngo", wrong coordinates) are obvious. But the handler checks them anyway, so why not return readable descriptions?

    +

    Better:

    +
    {
    +  "reason": "wrong_parameter_value",
    +  "localized_message":
    +    "Something is wrong.⮠
    +     Contact the developer of the app."
    +  "details": {
    +    "checks_failed": [
    +      {
    +        "field": "recipe",
    +        "error_type": "wrong_value",
    +        "message":
    +          "Unknown value: 'lngo'.⮠
    +           Did you mean 'lungo'?"
    +      },
    +      {
    +        "field": "position.latitude",
    +        "error_type": 
    +          "constraint_violation",
    +        "constraints": {
    +          "min": -90,
    +          "max": 90
    +        },
    +        "message":
    +          "'position.latitude' value⮠
    +            must fall within⮠
    +            the [-90, 90] interval"
    +      }
    +    ]
    +  }
    +}
    +
    +

    It is also a good practice to return all detectable errors at once to spare developers' time.

    +
    11. Maintain a proper error sequence
    +

    First, always return unresolvable errors before the resolvable ones:

    +
    POST /v1/orders
    +{
    +  "recipe": "lngo",
    +  "offer"
    +}
    +→ 409 Conflict
    +{
    +  "reason": "offer_expired"
    +}
    +// Request repeats
    +// with the renewed offer
    +POST /v1/orders
    +{
    +  "recipe": "lngo",
    +  "offer"
    +}
    +→ 400 Bad Request
    +{
    +  "reason": "recipe_unknown"
    +}
    +
    +

    — what was the point of renewing the offer if the order cannot be created anyway?

    +

    Second, maintain such a sequence of unresolvable errors which leads to a minimal amount of customers' and developers' irritation. In particular, this means returning the most significant errors first, solving which requires more effort.

    +

    Bad:

    +
    POST /v1/orders
    +{
    +  "items": [{
    +    "item_id": "123",
    +    "price": "0.10"
    +  }]
    +}
    +→
    +409 Conflict
    +{
    +  "reason": "price_changed",
    +  "details": [{
    +    "item_id": "123",
    +    "actual_price": "0.20"
    +  }]
    +}
    +// Request repeats
    +// with an actual price
    +POST /v1/orders
    +{
    +  "items": [{
    +    "item_id": "123",
    +    "price": "0.20"
    +  }]
    +}
    +→
    +409 Conflict
    +{
    +  "reason": "order_limit_exceeded",
    +  "localized_message":
    +    "Order limit exceeded"
    +}
    +
    +

    — what was the point of showing the price changed dialog, if the user still can't make an order, even if the price is right? When one of the concurrent orders has finished, and the user is able to commit another one, prices, item availability, and other order parameters will likely need another correction.

    +

    Third, draw a chart: which error resolution might lead to the emergence of another one. Otherwise, you might eventually return the same error several times, or worse, make a cycle of errors.

    +
    // Create an order
    +// with a paid delivery
    +POST /v1/orders
    +{
    +  "items": 3,
    +  "item_price": "3000.00"
    +  "currency_code": "MNT",
    +  "delivery_fee": "1000.00",
    +  "total": "10000.00"
    +}
    +→ 409 Conflict
    +// Error: if the order sum
    +// is more than 9000 tögrögs, 
    +// delivery must be free
    +{
    +  "reason": "delivery_is_free"
    +}
    +// Create an order
    +// with a free delivery
    +POST /v1/orders
    +{
    +  "items": 3,
    +  "item_price": "3000.00"
    +  "currency_code": "MNT",
    +  "delivery_fee": "0.00",
    +  "total": "9000.00"
    +}
    +→ 409 Conflict
    +// Error: minimal order sum
    +// is 10000 tögrögs
    +{
    +  "reason": "below_minimal_sum",
    +  "currency_code": "MNT",
    +  "minimal_sum": "10000.00"
    +}
    +
    +

    You may note that in this setup the error can't be resolved in one step: this situation must be elaborated over, and either order calculation parameters must be changed (discounts should not be counted against the minimal order sum), or a special type of error must be introduced.

    +

    Developing machine-readable interfaces

    +

    In pursuit of the API clarity for humans, we frequently forget that it's not developers themselves who interact with the endpoints, but the code they've written. Many concepts that work well with user interfaces, are badly suited for the program ones: specifically, developers can't make decisions based on textual information, and they can't ‘refresh’ the state in case of some confusing situation.

    +
    12. The system state must be observable by clients
    +

    Sometimes, program systems provide interfaces that do not expose to the clients all the data on what is now being executed on the user's +behalf, specifically — which operations are running and what their statuses are.

    Bad:

    // Creates an order and returns its id
     POST /v1/orders
    @@ -1562,162 +1877,190 @@ GET /v1/orders/{id}
     // in all statuses
     GET /v1/users/{id}/orders
     
    -
    10. Avoid double negations
    -

    Bad: "dont_call_me": false
    -— humans are bad at perceiving double negation; make mistakes.

    -

    Better: "prohibit_calling": true or "avoid_calling": true
    -— it's easier to read, though you shouldn't deceive yourself. Avoid semantical double negations, even if you've found a ‘negative’ word without a ‘negative’ prefix.

    -

    Also worth mentioning that making mistakes in the de Morgan's laws usage is even simpler. For example, if you have two flags:

    -
    GET /coffee-machines/{id}/stocks
    -→
    -{
    -  "has_beans": true,
    -  "has_cup": true
    -}
    -
    -

    ‘Coffee might be prepared’ condition would look like has_beans && has_cup — both flags must be true. However, if you provide the negations of both flags:

    +

    This rule is applicable to errors as well, especially client ones. If the error might be corrected, the related data must be machine-readable.

    +

    Bad: { "error": "email malformed" } +— the only thing developers might do with this error is to show the message to the end user.

    +

    Better:

    {
    -  "beans_absence": false,
    -  "cup_absence": false
    +  // Machine-readable status
    +  "status": "validation_failed",
    +  // An array; if there are several
    +  // errors, the user might correct
    +  // them all at once
    +  "failed_checks": [
    +     {
    +       "field: "email",
    +       "error_type": "malformed",
    +       // Localized 
    +       // human-readable message
    +       "message": "email malformed"
    +     }
    +  ]
     }
     
    -

    — then developers will have to evaluate one of the !beans_absence && !cup_absence!(beans_absence || cup_absence) conditions, and in this transition, people tend to make mistakes. Avoiding double negations helps little, and regretfully only general advice could be given: avoid the situations when developers have to evaluate such flags.

    -
    11. Avoid implicit type conversion
    -

    This advice is opposite to the previous one, ironically. When developing APIs you frequently need to add a new optional field with a non-empty default value. For example:

    -
    POST /v1/orders
    -{}
    -→
    -{
    -  "contactless_delivery": true
    -}
    -
    -

    This new contactless_delivery option isn't required, but its default value is true. A question arises: how developers should discern explicit intention to abolish the option (false) from knowing not it exists (field isn't set). They have to write something like:

    -
    if (Type(order.contactless_delivery) == 'Boolean' &&
    -    order.contactless_delivery == false) { … }
    -
    -

    This practice makes the code more complicated, and it's quite easy to make mistakes, which will effectively treat the field in a quite opposite manner. The same could happen if some special values (i.e. null or -1) to denote value absence are used.

    -

    The universal rule to deal with such situations is to make all new Boolean flags false by default.

    -

    Better

    -
    POST /v1/orders
    -{}
    -→
    -{
    -  "force_contact_delivery": false
    -}
    -
    -

    If a non-Boolean field with specially treated value absence is to be introduced, then introduce two fields.

    +
    13. Specify lifespans of resources and caching policies
    +

    In modern systems, clients usually have their own state and almost universally cache results of requests — no matter, session-wise or long-term, every entity has some period of autonomous existence. So it's highly desirable to make clarifications; it should be understandable how the data is supposed to be cached, if not from operation signatures, but at least from the documentation.

    +

    Let's stress that we understand ‘cache’ in the extended sense: which variation of operation parameters (not just the request time, but other variables as well) should be considered close enough to some previous request to use the cached result?

    Bad:

    -
    // Creates a user
    -POST /users
    -{ … }
    +
    // Returns lungo price in cafes
    +// closest to the specified location
    +GET /price?recipe=lungo⮠
    +  &longitude={longitude}⮠
    +  &latitude={latitude}
    +→
    +{ "currency_code", "price" }
    +
    +

    Two questions arise:

    +
      +
    • until when the price is valid?
    • +
    • in what vicinity of the location the price is valid?
    • +
    +

    Better: you may use standard protocol capabilities to denote cache options, like the Cache-Control header. If you need caching in both temporal and spatial dimensions, you should do something like that:

    +
    // Returns an offer: for what money sum
    +// our service commits to make a lungo
    +GET /price?recipe=lungo⮠
    +  &longitude={longitude}⮠
    +  &latitude={latitude}
     →
    -// Users are created with a monthly
    -// spending limit set by default
     {
    -  …
    -  "spending_monthly_limit_usd": "100"
    -}
    -// To cancel the limit null value is used
    -POST /users
    -{ 
    -  …
    -  "spending_monthly_limit_usd": null
    +  "offer": {
    +    "id",
    +    "currency_code",
    +    "price",
    +    "conditions": {
    +      // Until when the price is valid
    +      "valid_until",
    +      // What vicinity 
    +      // the price is valid within
    +      // * city
    +      // * geographical object
    +      // * …
    +      "valid_within"
    +    }
    +  }
     }
     
    -

    Better

    -
    POST /users
    -{
    -  // true — user explicitly cancels
    -  //   monthly spending limit
    -  // false — limit isn't canceled
    -  //   (default value)
    -  "abolish_spending_limit": false,
    -  // Non-required field
    -  // Only present if the previous flag
    -  // is set to false
    -  "spending_monthly_limit_usd": "100",
    -  …
    -}
    -
    -

    NB: the contradiction with the previous rule lies in the necessity of introducing ‘negative’ flags (the ‘no limit’ flag), which we had to rename to abolish_spending_limit. Though it's a decent name for a negative flag, its semantics is still unobvious, and developers will have to read the docs. That's the way.

    -
    12. Avoid implicit partial updates
    +
    14. Pagination, filtration, and cursors
    +

    Any endpoints returning data collections must be paginated. No exclusions exist.

    +

    Any paginated endpoint must provide an interface to iterate over all the data.

    Bad:

    -
    // Return the order state
    -// by its id
    -GET /v1/orders/123
    -→
    -{
    -  "order_id",
    -  "delivery_address",
    -  "client_phone_number",
    -  "client_phone_number_ext",
    -  "updated_at"
    -}
    -// Partially rewrites the order
    -PATCH /v1/orders/123
    -{ "delivery_address" }
    -→
    -{ "delivery_address" }
    +
    // Returns a limited number of records
    +// sorted by creation date
    +// starting with a record with an index
    +// equals to `offset`
    +GET /v1/records?limit=10&offset=100
     
    -

    — this approach is usually chosen to lessen request and response body sizes, plus it allows for the implementation of collaborative editing cheaply. Both these advantages are imaginary.

    -

    First, sparing bytes on semantic data is seldom needed in modern apps. Network packet sizes (MTU, Maximum Transmission Unit) are more than a kilobyte right now; shortening responses is useless while they're less than a kilobyte.

    -

    Excessive network traffic usually occurs if:

    +

    At the first glance, this is the most standard way of organizing the pagination in APIs. But let's ask ourselves some questions.

    +
      +
    1. How clients could learn about new records being added at the beginning of the list? +Obviously, a client could only retry the initial request (offset=0) and compare identifiers to those it already knows. But what if the number of new records exceeds the limit? Imagine the situation:
        -
      • no data pagination is provided;
      • -
      • no limits on field values are set;
      • -
      • binary data is transmitted (graphics, audio, video, etc.)
      • +
      • the client process records sequentially;
      • +
      • some problem occurred, and a batch of new records awaits processing;
      • +
      • the client requests new records (offset=0) but can't find any known records on the first page;
      • +
      • the client continues iterating over records page by page until it finds the last known identifier; all this time the order processing is idle;
      • +
      • the client might never start processing, being preoccupied with chaotic page requests to restore records sequence.
      -

      Transferring only a subset of fields solves none of these problems, in the best case just masks them. A more viable approach comprises:

      -
        -
      • making separate endpoints for ‘heavy’ data;
      • -
      • introducing pagination and field value length limits;
      • -
      • stopping saving bytes in all other cases.
      • -
      -

      Second, shortening response sizes will backfire exactly with spoiling collaborative editing: one client won't see the changes the other client has made. Generally speaking, in 9 cases out of 10, it is better to return a full entity state from any modifying operation, sharing the format with the read-access endpoint. Actually, you should always do this unless response size affects performance.

      -

      Third, this approach might work if you need to rewrite a field's value. But how to unset the field, e.g. to return its value to the default state? For example, how to remove the client_phone_number_ext?

      -

      In such cases, special values are often being used, for example, a null one. But as we discussed above, this is a defective practice. Another variant is prohibiting non-required fields, but that would pose considerable obstacles in a way of expanding the API.

      -

      Better: one of the following two strategies might be used.

      -

      Option #1: splitting the endpoints. Editable fields are grouped and taken out as separate endpoints. This approach also matches well against the decomposition principle we discussed in the previous chapter.

      -
      // Return the order state
      -// by its id
      -GET /v1/orders/123
      -→
      -{
      -  "order_id",
      -  "delivery_details": {
      -    "address"
      -  },
      -  "client_details": {
      -    "phone_number",
      -    "phone_number_ext"
      -  },
      -  "updated_at"
      -}
      -// Fully rewrite order delivery options
      -PUT /v1/orders/123/delivery-details
      -{ "address" }
      -// Fully rewrite order customer data
      -PUT /v1/orders/123/client-details
      -{ "phone_number" }
      +
    2. +
    3. What happens if some record is deleted from the head of the list?
      +Easy: the client will miss one record and will never learn this.
    4. +
    5. What cache parameters to set for this endpoint?
      +None could be set: repeating the request with the same limit and offset parameters each time produces a new record set.
    6. +
    +

    Better: in such unidirectional lists the pagination must use the key that implies the order. Like this:

    +
    // Returns a limited number of records
    +// sorted by creation date
    +// starting with a record with an identifier
    +// following the specified one
    +GET /v1/records⮠
    +  ?older_than={record_id}&limit=10
    +// Returns a limited number of records
    +// sorted by creation date
    +// starting with a record with an identifier
    +// preceding the specified one
    +GET /v1/records⮠
    +  ?newer_than={record_id}&limit=10
     
    -

    Omitting the client_phone_number_ext in the PUT client-details request would be sufficient to remove it. This approach also helps to separate constant and calculated fields (order_id and updated_at) from editable ones, thus getting rid of ambiguous situations (what happens if a client tries to rewrite the updated_at field?). You may also return the entire order entity from PUT endpoints (however, there should be some naming convention for that).

    -

    Option 2: design a format for atomic changes.

    -
    POST /v1/order/changes
    -X-Idempotency-Token: <see next paragraph>
    +

    With the pagination organized like that, clients never bother about records being added or removed in the processed part of the list: they continue to iterate over the records, either getting new ones (using newer_than) or older ones (using older_than). If there is no record removal operation, clients may easily cache responses — the URL will always return the same record set.

    +

    Another way to organize such lists is by returning a cursor to be used instead of the record_id, making interfaces more versatile.

    +
    // Initial data request
    +POST /v1/records/list
     {
    -  "changes": [{
    -    "type": "set",
    -    "field": "delivery_address",
    -    "value": <new value>
    -  }, {
    -    "type": "unset",
    -    "field": "client_phone_number_ext"
    +  // Some additional filtering options
    +  "filter": {
    +    "category": "some_category",
    +    "created_date": {
    +      "older_than": "2020-12-07"
    +    }
    +  }
    +}
    +→
    +{ "cursor" }
    +
    +
    // Follow-up requests
    +GET /v1/records?cursor=<cursor value>
    +{ "records", "cursor" }
    +
    +

    One advantage of this approach is the possibility to keep initial request parameters (i.e. the filter in our example) embedded into the cursor itself, thus not copying them in follow-up requests. It might be especially actual if the initial request prepares the full dataset, for example, moving it from the ‘cold’ storage to a ‘hot’ one (then the cursor might simply contain the encoded dataset id and the offset).

    +

    There are several approaches to implementing cursors (for example, making a single endpoint for initial and follow-up requests and returning the first data portion in the first response). As usual, the crucial part is maintaining consistency across all such endpoints.

    +

    NB: some sources discourage this approach because in this case user can't see a list of all pages and can't choose an arbitrary one. We should note here that:

    +
      +
    • such a case (pages list and page selection) exists if we deal with user interfaces; we could hardly imagine a program interface that needs to provide access to random data pages;
    • +
    • if we still talk about an API to some application, which has a ‘paging’ user control, then a proper approach would be to prepare ‘paging’ data on the server side, including generating links to pages;
    • +
    • cursor-based solutions don't prohibit using the offset/limit parameters; nothing could prevent us from creating a dual interface, which might serve both GET /items?cursor=… and GET /items?offset=…&limit=… requests;
    • +
    • finally, if there is a necessity to provide access to arbitrary pages in the user interface, we should ask ourselves a question, which problem is being solved that way; probably, users use this functionality to find something: a specific element on the list, or the position they ended while working with the list last time; probably, we should provide more convenient controls to solve those tasks than accessing data pages by their indexes.
    • +
    +

    Bad:

    +
    // Returns a limited number of records
    +// sorted by a specified field 
    +// in a specified order
    +// starting with a record with an index
    +// equals to `offset`
    +GET /records?sort_by=date_modified⮠
    +  &sort_order=desc&limit=10&offset=100
    +
    +

    Sorting by the date of modification usually means that data might be modified. In other words, some records might change after the first data chunk is returned, but before the next chunk is requested. Modified records will simply disappear from the listing because of moving to the first page. Clients will never get those records that were changed during the iteration process, even if the cursor-based scheme is implemented, and they never learn the sheer fact of such an omission. Also, this particular interface isn't extendable as there is no way to add sorting by two or more fields.

    +

    Better: there is no general solution to this problem in this formulation. Listing records by modification time will always be unpredictably volatile, so we have to change the approach itself; we have two options.

    +

    Option one: fix the records ordering at the moment we've got the initial request, e.g. our server produces the entire list and stores it in the immutable form:

    +
    // Creates a view based on the parameters passed
    +POST /v1/record-views
    +{
    +  sort_by: [{ 
    +    "field": "date_modified", 
    +    "order": "desc" 
       }]
     }
    +→
    +{ "id", "cursor" }
     
    -

    This approach is much harder to implement, but it's the only viable method to implement collaborative editing since it explicitly reflects what a user was actually doing with entity representation. With data exposed in such a format, you might actually implement offline editing, when user changes are accumulated and then sent at once, while the server automatically resolves conflicts by ‘rebasing’ the changes.

    -
    13. All API operations must be idempotent
    -

    Let us recall that idempotency is the following property: repeated calls to the same function with the same parameters don't change the resource state. Since we're discussing client-server interaction in the first place, repeating requests in case of network failure isn't an exception, but a norm of life.

    +
    // Returns a portion of the view
    +GET /v1/record-views/{id}⮠
    +  ?cursor={cursor}
    +
    +

    Since the produced view is immutable, access to it might be organized in any form, including a limit-offset scheme, cursors, Range header, etc. However, there is a downside: records modified after the view was generated will be misplaced or outdated.

    +

    Option two: guarantee a strict records order, for example, by introducing a concept of record change events:

    +
    POST /v1/records/modified/list
    +{
    +  // Optional
    +  "cursor"
    +}
    +→
    +{
    +  "modified": [
    +    { "date", "record_id" }
    +  ],
    +  "cursor"
    +}
    +
    +

    This scheme's downsides are the necessity to create separate indexed event storage, and the multiplication of data items, since for a single record many events might exist.

    +

    Ensuring technical quality of APIs

    +

    Fine APIs must not only solve developers' and end users' problems but also ensure the quality of the solution, e.g. do not contain logical and technical mistakes (and do not provoke developers to make them), save computational resources, and in general implement the best practices applicable to the subject area.

    +
    15. Keep the precision of fractional numbers 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.

    +

    If conversion to a float number will certainly lead to losing the precision (let's say if we translate ‘20 minutes’ into hours as a decimal fraction), it's better to either stick to a fully precise format (e.g. opt for 00:20 instead of 0.33333…) or to provide an SDK to work with this data, or as a last resort describe the rounding principles in the documentation.

    +
    16. All API operations must be idempotent
    +

    Let us remind the reader that idempotency is the following property: repeated calls to the same function with the same parameters won't change the resource state. Since we're discussing client-server interaction in the first place, repeating requests in case of network failure isn't an exception, but a norm of life.

    If the endpoint's idempotency can't be assured naturally, explicit idempotency parameters must be added, in a form of either a token or a resource version.

    Bad:

    // Creates an order
    @@ -1745,7 +2088,7 @@ PUT /v1/orders/drafts/{draft_id}
     

    Also worth mentioning that adding idempotency tokens to naturally idempotent handlers isn't meaningless either, since it allows to distinguish two situations:

    • a client didn't get the response because of some network issues, and is now repeating the request;
    • -
    • a client's mistaken, trying to make conflicting changes.
    • +
    • a client made a mistake by posting conflicting requests.

    Consider the following example: imagine there is a shared resource, characterized by a revision number, and a client tries updating it.

    POST /resource/updates
    @@ -1754,7 +2097,7 @@ PUT /v1/orders/drafts/{draft_id}
       "updates"
     }
     
    -

    The server retrieves the actual resource revision and finds it to be 124. How to respond correctly? 409 Conflict might be returned, but then the client will be forced to understand the nature of the conflict and somehow resolve it, potentially confusing the user. It's also unwise to fragment the conflict resolving algorithm, allowing each client to implement it independently.

    +

    The server retrieves the actual resource revision and finds it to be 124. How to respond correctly? 409 Conflict might be returned, but then the client will be forced to understand the nature of the conflict and somehow resolve it, potentially confusing the user. It's also unwise to fragment the conflict-resolving algorithm, allowing each client to implement it independently.

    The server may compare request bodies, assuming that identical updates values mean retrying, but this assumption might be dangerously wrong (for example if the resource is a counter of some kind, then repeating identical requests are routine).

    Adding the idempotency token (either directly as a random string, or indirectly in a form of drafts) solves this problem.

    POST /resource/updates
    @@ -1776,13 +2119,13 @@ X-Idempotency-Token: <token>
     → 409 Conflict
     

    — the server found out that a different token was used in creating revision 124, which means an access conflict.

    -

    Furthermore, adding idempotency tokens not only resolves the issue but also makes advanced optimizations possible. If the server detects an access conflict, it could try to resolve it, ‘rebasing’ the update like modern version control systems do, and return a 200 OK instead of a 409 Conflict. This logic dramatically improves user experience, being fully backwards compatible, and helps to avoid conflict resolving code fragmentation.

    +

    Furthermore, adding idempotency tokens not only resolves the issue but also makes advanced optimizations possible. If the server detects an access conflict, it could try to resolve it, ‘rebasing’ the update like modern version control systems do, and return a 200 OK instead of a 409 Conflict. This logic dramatically improves user experience, being fully backwards compatible, and helps to avoid conflict-resolving code fragmentation.

    Also, be warned: clients are bad at implementing idempotency tokens. Two problems are common:

    • you can't really expect that clients generate truly random tokens — they may share the same seed or simply use weak algorithms or entropy sources; therefore you must put constraints on token checking: token must be unique to a specific user and resource, not globally;
    • clients tend to misunderstand the concept and either generate new tokens each time they repeat the request (which deteriorates the UX, but otherwise healthy) or conversely use one token in several requests (not healthy at all and could lead to catastrophic disasters; another reason to implement the suggestion in the previous clause); writing detailed doc and/or client library is highly recommended.
    -
    14. Avoid non-atomic operations
    +
    17. Avoid non-atomic operations

    There is a common problem with implementing the changes list approach: what to do if some changes were successfully applied, while others are not? The rule is simple: if you may ensure the atomicity (e.g. either apply all changes or none of them) — do it.

    Bad:

    // Returns a list of recipes
    @@ -1824,7 +2167,7 @@ GET /v1/recipes
       }]
     }
     
    -

    — there is no way how the client might learn that failed operation was actually partially applied. Even if there is an indication of this fact in the response, the client still cannot tell, whether lungo volume changed because of the request, or some other client changed it.

    +

    — there is no way how the client might learn that failed operation was actually partially applied. Even if there is an indication of this fact in the response, the client still cannot tell, whether the lungo volume changed because of the request, or if some other client changed it.

    If you can't guarantee the atomicity of an operation, you should elaborate in detail on how to deal with it. There must be a separate status for each individual change.

    Better:

    PATCH /v1/recipes
    @@ -1888,7 +2231,8 @@ GET /v1/recipes
         …
         "status": "fail",
         "error": {
    -      "reason": "too_many_requests"
    +      "reason": 
    +        "too_many_requests"
         }
       }]
     }
    @@ -1919,325 +2263,189 @@ GET /v1/recipes
     

    To the client, everything looks normal: changes were applied, and the last response got is always actual. But the resource state after the first request was inherently different from the resource state after the second one, which contradicts the very definition of ‘idempotency’.

    It would be more correct if the server did nothing upon getting the second request with the same idempotency token, and returned the same status list breakdown. But it implies that storing these breakdowns must be implemented.

    Just in case: nested operations must be idempotent themselves. If they are not, separate idempotency tokens must be generated for each nested operation.

    -
    15. Specify caching policies
    -

    Client-server interaction usually implies that network and server resources are limited, therefore caching operation results on client devices is standard practice.

    -

    So it's highly desirable to make caching options clear, if not from functions' signatures then at least from docs.

    -

    Bad:

    -
    // Returns lungo price in cafes
    -// closest to the specified location
    -GET /price?recipe=lungo
    -  &longitude={longitude}&latitude={latitude}
    -→
    -{ "currency_code", "price" }
    -
    -

    Two questions arise:

    +
    18. Don't invent security
    +

    If the author of this book was given a dollar each time he had to implement the additional security protocol invented by someone, he would already retire. The API developers' passion for signing request parameters or introducing complex schemes of exchanging passwords for tokens is as obvious as meaningless.

    +

    First, almost all security-enhancing procedures for every kind of operation are already invented. There is no need to re-think them anew; just take the existing approach and implement it. No self-invented algorithm for request signature checking provides the same level of preventing Man-in-the-Middle attack as a TLS connection with mutual certificate pinning.

    +

    Second, it's quite presumptuous (and dangerous) to assume you're an expert in security. New attack vectors come every day, and being aware of all the actual threats is a full-day job. If you do something different during workdays, the security system designed by you will contain vulnerabilities that you have never heard about — for example, your password-checking algorithm might be susceptible to the timing attack, and your web-server, to the request splitting attack.

    +
    19. Explicitly declare technical restrictions
    +

    Every field in your API comes with restrictions: the maximum allowed text length, the size of attached documents, the allowed ranges for numeric values, etc. Often, describing those limits is neglected by API developers — either because they consider it obvious, or because they simply don't know the boundaries themselves. This is of course an antipattern: not knowing what are the limits automatically implies that partners' code might stop working at any moment because of the reasons they don't control.

    +

    Therefore, first, declare the boundaries for every field in the API without any exceptions, and, second, generate proper machine-readable errors describing which exact boundary was violated should such a violation occur.

    +

    The same reasoning applies to quotas as well: partners must have access to the statistics on which part of the quota they have already used, and the errors in the case of exceeding quotas must be informative.

    +
    20. Count the amount of traffic
    +

    Nowadays the amount of traffic is rarely taken into account — the Internet connection is considered unlimited almost universally. However, it's still not entirely unlimited: with some degree of carelessness, it's always possible to design a system generating the amount of traffic that is uncomfortable even for modern networks.

    +

    There are three obvious reasons for inflating network traffic:

      -
    • until when the price is valid?
    • -
    • in what vicinity of the location the price is valid?
    • +
    • no data pagination provided;
    • +
    • no limits on the data fields set, or too large binary data (graphics, audio, video, etc.) is being transmitted;
    • +
    • clients query for the data too frequently or cache them too little.
    -

    Better: you may use standard protocol capabilities to denote cache options, like the Cache-Control header. If you need caching in both temporal and spatial dimensions, you should do something like that:

    -
    // Returns an offer: for what money sum
    -// our service commits to make a lungo
    -GET /price?recipe=lungo
    -  &longitude={longitude}&latitude={latitude}
    -→
    -{
    -  "offer": {
    -    "id",
    -    "currency_code",
    -    "price",
    -    "conditions": {
    -      // Until when the price is valid
    -      "valid_until",
    -      // What vicinity the price is valid within
    -      // * city
    -      // * geographical object
    -      // * …
    -      "valid_within"
    -    }
    -  }
    -}
    -
    -
    16. Pagination, filtration, and cursors
    -

    Any endpoints returning data collections must be paginated. No exclusions exist.

    -

    Any paginated endpoint must provide an interface to iterate over all the data.

    -

    Bad:

    -
    // Returns a limited number of records
    -// sorted by creation date
    -// starting with a record with an index
    -// equals to `offset`
    -GET /v1/records?limit=10&offset=100
    -
    -

    At the first glance, this is the most standard way of organizing the pagination in APIs. But let's ask some questions to ourselves.

    -
      -
    1. How clients could learn about new records being added at the beginning of the list? -Obviously, a client could only retry the initial request (offset=0) and compare identifiers to those it already knows. But what if the number of new records exceeds the limit? Imagine the situation: +

      If the first two problems are solved by applying pure technical measures (see the corresponding paragraphs), the third one is more of a logical kind: how to organize the client updates stream to find a balance between the responsiveness of the system and the resources spent to ensure it. Here are several recommendations:

        -
      • the client process records sequentially;
      • -
      • some problem occurred, and a batch of new records awaits processing;
      • -
      • the client requests new records (offset=0) but can't find any known records on the first page;
      • -
      • the client continues iterating over records page by page until it finds the last known identifier; all this time the order processing is idle;
      • -
      • the client might never start processing, being preoccupied with chaotic page requests to restore records sequence.
      • +
      • +

        do not rely too heavily on asynchronous interfaces;

        +
          +
        • on one side, they allow tackling many technical problems related to the API performance, which, in turn, allows for maintaining backwards compatibility: if some method is asynchronous from the very beginning, the latencies and the data consistency models might be easily tuned if needed;
        • +
        • from the other side, the number of requests clients generate becomes hardly predicable, as a client in order to retrieve a result needs to make some unpredictable number of attempts;
      • -
      • What happens if some record is deleted from the head of the list?
        -Easy: the client will miss one record and will never learn this.
      • -
      • What cache parameters to set for this endpoint?
        -None could be set: repeating the request with the same limit and offset parameters each time produces a new record set.
      • -
    -

    Better: in such unidirectional lists the pagination must use the key that implies the order. Like this:

    -
    // Returns a limited number of records
    -// sorted by creation date
    -// starting with a record with an identifier
    -// following the specified one
    -GET /v1/records?older_than={record_id}&limit=10
    -// Returns a limited number of records
    -// sorted by creation date
    -// starting with a record with an identifier
    -// preceding the specified one
    -GET /v1/records?newer_than={record_id}&limit=10
    -
    -

    With the pagination organized like that, clients never bother about records being added or removed in the processed part of the list: they continue to iterate over the records, either getting new ones (using newer_than) or older ones (using older_than). If there is no record removal operation, clients may easily cache responses — the URL will always return the same record set.

    -

    Another way to organize such lists is returning a cursor to be used instead of the record_id, making interfaces more versatile.

    -
    // Initial data request
    -POST /v1/records/list
    +
  • +

    declare an explicit retry policy (for example, with the Retry-After header);

    +
      +
    • yes, some partners will ignore it as developers will get too lazy to implement it, but some will not (especially if you provide the SDKs as well);
    • +
    +
  • +
  • +

    if you expect a significant number of asynchronous operations in the API, allow developers to choose between the poll model (clients make repeated requests to an endpoint to check the asynchronous procedure status) and the push model (the server notifies clients of status changes, for example, via webhooks or server-push mechanics);

    +
  • +
  • +

    if some entity comprises both ‘lightweight’ data (let's say, the name and the description of the recipe) and ‘heavy’ data (let's say, the promo picture of the beverage which might easily be a hundred times larger than the text fields), it's better to split endpoints and pass only a reference to the ‘heavy’ data (a link to the image, in our case) — this will allow at least setting different cache policies for different kinds of data.

    +
  • + +

    As a useful exercise, try modeling the typical lifecycle of a partner's app's main functionality (for example, making a single order) to count the number of requests and the amount of traffic that it takes.

    +
    21. Avoid implicit partial updates
    +

    One of the most common API design antipatterns is an attempt to spare something on detailed state change descriptions.

    +

    Bad:

    +
    // Creates an order comprising
    +// two items
    +POST /v1/orders/
     {
    -  // Some additional filtering options
    -  "filter": {
    -    "category": "some_category",
    -    "created_date": {
    -      "older_than": "2020-12-07"
    -    }
    +  "delivery_address",
    +  "items": [{
    +    "recipe": "lungo",
    +  }, {
    +    "recipe": "latte",
    +    "milk_type": "oats"
    +  }]
    +}
    +→
    +{ "order_id" }
    +
    +
    // Partially rewrites the order,
    +// updates the volume
    +// of the second item
    +PATCH /v1/orders/{id}
    +{
    +  "items": [null, {
    +    "volume": "800ml"
    +  }]
    +}
    +→
    +{ /* updates accepted */ }
    +
    +

    This signature is bad per se as it's unreadable. What does null as the first array element mean — is it a deletion of an element or an indication that no actions are needed towards it? What happens with the fields that are not stated in the update operation body (delivery_address, milk_type) — will they be reset to defaults, or stay unchanged?

    +

    The nastiest part is that whatever option you choose, the number of problems will only multiply further. Let's say we agreed that the {"items":[null, {…}]} statement means that the first element of the array is left untouched, e.g. no changes are needed. Then, how shall we encode its deletion? Invent one more ‘magical’ value meaning ‘remove it’? Similarly, if the fields that are not explicitly mentioned retain their value — how to reset them to defaults?

    +

    The simple solution is always rewriting the data entirely, e.g. to require passing the entire object, to replace the current state with it, and to return the full state as a result of the operation. This obvious solution is frequently rejected with the following reasoning:

    +
      +
    • increased requests sizes and therefore, the amount of traffic;
    • +
    • the necessity to detect which fields are changed (for instance, to generate proper state change events for subscribers);
    • +
    • the inability of organizing cooperative editing when two clients are editing different object properties simultaneously.
    • +
    +

    However, if we take a deeper look, all these disadvantages are actually imaginative:

    +
      +
    • the reasons for increasing the amount of traffic were described in the previous paragraphs, and serving extra fields is not one of them (and if it is, it's rather a rationale to decompose the endpoint);
    • +
    • the concept of sending only those fields that changed is in fact about shifting the responsibility of change detection to clients; +
        +
      • it doesn't make the task any easier, and also introduces the problem of client code fragmentation as several independent implementations of the change detection algorithm will occur;
      • +
      • furthermore, the existence of the client algorithm for finding the fields that changed doesn't mean that the server might skip implementing it as client developers might make mistakes or simply spare the effort and always send all the fields;
      • +
      +
    • +
    • finally, this naïve approach to organizing collaborative editing works only with transitive changes (e.g. if the final result does not depend on the order in which the operations were executed), and in our case, it's already not true: deletion of the first element and editing the second element are non-transitive; +
        +
      • often, in addition to sparing traffic on requests, the same concept is applied to responses as well, e.g. no data is returned for modifying operations; thus two clients making simultaneous edits do not see one another's changes.
      • +
      +
    • +
    +

    Better: split the functionality. This also correlates well with the decomposition principle we've discussed in the previous chapter.

    +
    // Creates an order comprising
    +// two items
    +POST /v1/orders/
    +{
    +  "parameters": {
    +    "delivery_address"
       }
    +  "items": [{
    +    "recipe": "lungo",
    +  }, {
    +    "recipe": "latte",
    +    "milk_type": "oats"
    +  }]
     }
     →
     {
    -  "cursor"
    -}
    -
    -
    // Follow-up requests
    -GET /v1/records?cursor=<cursor value>
    -{ "records", "cursor" }
    -
    -

    One advantage of this approach is the possibility to keep initial request parameters (i.e. the filter in our example) embedded into the cursor itself, thus not copying them in follow-up requests. It might be especially actual if the initial request prepares the full dataset, for example, moving it from the ‘cold’ storage to a ‘hot’ one (then the cursor might simply contain the encoded dataset id and the offset).

    -

    There are several approaches to implementing cursors (for example, making a single endpoint for initial and follow-up requests, returning the first data portion in the first response). As usual, the crucial part is maintaining consistency across all such endpoints.

    -

    NB: some sources discourage this approach because in this case user can't see a list of all pages and can't choose an arbitrary one. We should note here that:

    -
      -
    • such a case (pages list and page selection) exists if we deal with user interfaces; we could hardly imagine a program interface that needs to provide access to random data pages;
    • -
    • if we still talk about an API to some application, which has a ‘paging’ user control, then a proper approach would be to prepare ‘paging’ data on the server side, including generating links to pages;
    • -
    • cursor-based solutions don't prohibit using the offset/limit parameters; nothing could prevent us from creating a dual interface, which might serve both GET /items?cursor=… and GET /items?offset=…&limit=… requests;
    • -
    • finally, if there is a necessity to provide access to arbitrary pages in the user interface, we should ask ourselves a question, which problem is being solved that way; probably, users use this functionality to find something: a specific element on the list, or the position they ended while working with the list last time; probably, we should provide more convenient controls to solve those tasks than accessing data pages by their indexes.
    • -
    -

    Bad:

    -
    // Returns a limited number of records
    -// sorted by a specified field in a specified order
    -// starting with a record with an index
    -// equals to `offset`
    -GET /records?sort_by=date_modified&sort_order=desc&limit=10&offset=100
    -
    -

    Sorting by the date of modification usually means that data might be modified. In other words, some records might change after the first data chunk is returned, but before the next chunk is requested. Modified records will simply disappear from the listing because of moving to the first page. Clients will never get those records that were changed during the iteration process, even if the cursor-based scheme is implemented, and they never learn the sheer fact of such an omission. Also, this particular interface isn't extendable as there is no way to add sorting by two or more fields.

    -

    Better: there is no general solution to this problem in this formulation. Listing records by modification time will always be unpredictably volatile, so we have to change the approach itself; we have two options.

    -

    Option one: fix the records ordering at the moment we've got the initial request, e.g. our server produces the entire list and stores it in the immutable form:

    -
    // Creates a view based on the parameters passed
    -POST /v1/record-views
    -{
    -  sort_by: [
    -    { "field": "date_modified", "order": "desc" }
    +  "order_id", 
    +  "created_at",
    +  "parameters": {
    +    "delivery_address"
    +  }
    +  "items": [
    +    { "item_id", "status"}, 
    +    { "item_id", "status"}
       ]
     }
    +
    +
    // Changes the order parameters
    +// that affect all items
    +PUT /v1/orders/{id}/parameters
    +{ "delivery_address" }
     →
    -{ "id", "cursor" }
    +{ "delivery_address" }
     
    -
    // Returns a portion of the view
    -GET /v1/record-views/{id}?cursor={cursor}
    -
    -

    Since the produced view is immutable, access to it might be organized in any form, including a limit-offset scheme, cursors, Range header, etc. However, there is a downside: records modified after the view was generated will be misplaced or outdated.

    -

    Option two: guarantee a strict records order, for example, by introducing a concept of record change events:

    -
    POST /v1/records/modified/list
    -{
    -  // Optional
    -  "cursor"
    +
    // Partially updates one item,
    +// sets the volume of one of
    +// the beverages
    +PUT /v1/orders/{id}/items/{item_id}
    +{ 
    +  // All the fields are passed,
    +  // even if only one has changed
    +  "recipe", "volume", "milk_type"
     }
     →
    +{ "recipe", "volume", "milk_type" }
    +
    +
    // Deletes one order item
    +DELETE /v1/orders/{id}/items/{item_id}
    +
    +

    Now to reset volume to its default value it's enough to omit it in the PUT /items/{item_id} request body. Also, the operations of deleting one item while simultaneously modifying another one are now transitive.

    +

    This approach also allows for separating non-mutable and calculated fields (in our case, created_at and status) from editable ones without creating ambiguous situations (what should happen if a client tries to change the created_at field?)

    +

    It is also possible to return full order objects from PUT endpoints instead of just the sub-resource that was overwritten (though it requires some naming convention).

    +

    NB: while decomposing endpoints, the idea of splitting them into mutable and non-mutable data often looks tempting. It makes possible to mark the latter as infinitely cacheable and never bother about pagination ordering and update format consistency. The plan looks solid on paper, but with the API expansion, it frequently happens that immutable fields eventually cease being immutable, and the entire concept not only stops working properly but even starts looking like a design flaw. We would rather recommend designating data as immutable in one of the two cases: (1) making them editable will really mean breaking backwards compatibility, or (2) the link to the resource (for example, an image) is served via the API as well, and you do possess the capability of making those links persistent (e.g. you might generate a new link to the image instead of rewriting the contents of the old one).

    +

    Even better: design a format for atomic changes.

    +
    POST /v1/order/changes
    +X-Idempotency-Token: <idempotency token>
     {
    -  "modified": [
    -    { "date", "record_id" }
    -  ],
    -  "cursor"
    +  "changes": [{
    +    "type": "set",
    +    "field": "delivery_address",
    +    "value": <new value>
    +  }, {
    +    "type": "unset_item_field",
    +    "item_id",
    +    "field": "volume"
    +  }],
    +  …
     }
     
    -

    This scheme's downsides are the necessity to create separate indexed event storage, and the multiplication of data items, since for a single record many events might exist.

    -
    17. Errors must be informative
    -

    While writing the code developers face problems, many of them quite trivial, like invalid parameter types or some boundary violations. The more convenient are the error responses your API return, the less is the amount of time developers waste struggling with it, and the more comfortable is working with the API.

    -

    Bad:

    -
    POST /v1/coffee-machines/search
    -{
    -  "recipes": ["lngo"],
    -  "position": {
    -    "latitude": 110,
    -    "longitude": 55
    -  }
    -}
    -→ 400 Bad Request
    -{}
    -
    -

    — of course, the mistakes (typo in the "lngo", wrong coordinates) are obvious. But the handler checks them anyway, why not return readable descriptions?

    -

    Better:

    -
    {
    -  "reason": "wrong_parameter_value",
    -  "localized_message":
    -    "Something is wrong. Contact the developer of the app."
    -  "details": {
    -    "checks_failed": [
    -      {
    -        "field": "recipe",
    -        "error_type": "wrong_value",
    -        "message":
    -          "Unknown value: 'lngo'. Did you mean 'lungo'?"
    -      },
    -      {
    -        "field": "position.latitude",
    -        "error_type": "constraint_violation",
    -        "constraints": {
    -          "min": -90,
    -          "max": 90
    -        },
    -        "message":
    -          "'position.latitude' value must fall within [-90, 90] interval"
    -      }
    -    ]
    -  }
    -}
    -
    -

    It is also a good practice to return all detectable errors at once to spare developers' time.

    -
    18. Maintain a proper error sequence
    -

    First, always return unresolvable errors before the resolvable ones:

    -
    POST /v1/orders
    -{
    -  "recipe": "lngo",
    -  "offer"
    -}
    -→ 409 Conflict
    -{
    -  "reason": "offer_expired"
    -}
    -// Request repeats
    -// with the renewed offer
    -POST /v1/orders
    -{
    -  "recipe": "lngo",
    -  "offer"
    -}
    -→ 400 Bad Request
    -{
    -  "reason": "recipe_unknown"
    -}
    -
    -

    — what was the point of renewing the offer if the order cannot be created anyway?

    -

    Second, maintain such a sequence of unresolvable errors which leads to a minimal amount of customers' and developers' irritation.

    -

    Bad:

    -
    POST /v1/orders
    -{
    -  "items": [{ "item_id": "123", "price": "0.10" }]
    -}
    -→
    -409 Conflict
    -{
    -  "reason": "price_changed",
    -  "details": [{ "item_id": "123", "actual_price": "0.20" }]
    -}
    -// Request repeats
    -// with an actual price
    -POST /v1/orders
    -{
    -  "items": [{ "item_id": "123", "price": "0.20" }]
    -}
    -→
    -409 Conflict
    -{
    -  "reason": "order_limit_exceeded",
    -  "localized_message": "Order limit exceeded"
    -}
    -
    -

    — what was the point of showing the price changed dialog, if the user still can't make an order, even if the price is right? When one of the concurrent orders finishes, and the user is able to commit another one, prices, items availability, and other order parameters will likely need another correction.

    -

    Third, draw a chart: which error resolution might lead to the emergence of another one. Otherwise, you might eventually return the same error several times, or worse, make a cycle of errors.

    -
    // Create an order
    -// with a payed delivery
    -POST /v1/orders
    -{
    -  "items": 3,
    -  "item_price": "3000.00"
    -  "currency_code": "MNT",
    -  "delivery_fee": "1000.00",
    -  "total": "10000.00"
    -}
    -→ 409 Conflict
    -// Error: if the order sum
    -// is more than 9000 tögrögs, 
    -// delivery must be free
    -{
    -  "reason": "delivery_is_free"
    -}
    -// Create an order
    -// with a free delivery
    -POST /v1/orders
    -{
    -  "items": 3,
    -  "item_price": "3000.00"
    -  "currency_code": "MNT",
    -  "delivery_fee": "0.00",
    -  "total": "9000.00"
    -}
    -→ 409 Conflict
    -// Error: minimal order sum
    -// is 10000 tögrögs
    -{
    -  "reason": "below_minimal_sum",
    -  "currency_code": "MNT",
    -  "minimal_sum": "10000.00"
    -}
    -
    -

    You may note that in this setup the error can't be resolved in one step: this situation must be elaborated over, and either order calculation parameters must be changed (discounts should not be counted against the minimal order sum), or a special type of error must be introduced.

    -
    19. Stipulate future restrictions
    -

    With the API popularity growth, it will inevitably become necessary to introduce technical means of preventing illicit API usage, such as displaying captcha, setting honeypots, raising the ‘too many requests’ exceptions, installing anti-DDoS proxies, etc. All these things cannot be done if the corresponding errors and messages were not described in the docs from the very beginning.

    +

    This approach is much harder to implement, but it's the only viable method to implement collaborative editing since it explicitly reflects what a user was actually doing with entity representation. With data exposed in such a format, you might actually implement offline editing, when user changes are accumulated and then sent at once, while the server automatically resolves conflicts by ‘rebasing’ the changes.

    +

    Ensuring API product quality

    +

    Apart from the technological limitations, any real API will soon face the imperfection of the surrounding reality. Of course, any one of us would prefer living in the world of pink unicorns, free of piles of legacy code, evil-doers, national conflicts, and competitors' scheming. Fortunately or not, we live in the real world, and API vendors have to mind all those while developing the API.

    +
    22. Use globally unique identifiers
    +

    It's considered a good form to use globally unique strings as entity identifiers, either semantic (i.e. "lungo" for beverage types) or random ones (i.e. UUID-4). It might turn out to be extremely useful if you need to merge data from several sources under a single identifier.

    +

    In general, we tend to advise using urn-like identifiers, e.g. urn:order:<uuid> (or just order:<uuid>). That helps a lot in dealing with legacy systems with different identifiers attached to the same entity. Namespaces in urns help to understand quickly which identifier is used and if there is a usage mistake.

    +

    One important implication: never use increasing numbers as external identifiers. Apart from the abovementioned reasons, it allows counting how many entities of each type there are in the system. Your competitors will be able to calculate a precise number of orders you have each day, for example.

    +

    NB: in this book, we often use short identifiers like "123" in code examples; that's for the convenience of reading the book on small screens. Do not replicate this practice in a real-world API.

    +
    23. Stipulate future restrictions
    +

    With the API popularity growth, it will inevitably become necessary to introduce technical means of preventing illicit API usage, such as displaying captchas, setting honeypots, raising the ‘too many requests’ exceptions, installing anti-DDoS proxies, etc. All these things cannot be done if the corresponding errors and messages were not described in the docs from the very beginning.

    You are not obliged to actually generate those exceptions, but you might stipulate this possibility in the terms of service. For example, you might describe the 429 Too Many Requests error or captcha redirect, but implement the functionality when it's actually needed.

    -

    It is extremely important to leave a room for multi-factored authentication (such as TOTP, SMS, or 3D-secure-like technologies) in case it's possible to make payments through the API. In this case, it's a must have from the very beginning.

    -
    20. Don't provide endpoints for mass downloading of sensitive data
    +

    It is extremely important to leave room for multi-factored authentication (such as TOTP, SMS, or 3D-secure-like technologies) if it's possible to make payments through the API. In this case, it's a must-have from the very beginning.

    +
    24. Don't provide endpoints for mass downloading of sensitive data

    If it's possible to get through the API users' personal data, bank card numbers, private messages, or any other kind of information, exposing of which might seriously harm users, partners, and/or you — there must be no methods of bulk getting the data, or at least there must be rate limiters, page size restrictions, and, ideally, multi-factored authentication in front of them.

    -

    Often, making such offloads on an ad-hoc basis, e.g. in bypass of the API, is a reasonable practice.

    -
    21. No results is a result
    -

    If a server processed a request correctly and no exceptional situation occurred — there must be no error. Regretfully, an antipattern is widespread — of throwing errors when zero results are found.

    -

    Bad

    -
    POST /search
    -{
    -  "query": "lungo",
    -  "location": <customer's location>
    -}
    -→ 404 Not Found
    -{
    -  "localized_message":
    -    "No one makes lungo nearby"
    -}
    -
    -

    4xx statuses imply that a client made a mistake. But no mistakes were made by either a customer or a developer: a client cannot know whether the lungo is served in this location beforehand.

    -

    Better:

    -
    POST /search
    -{
    -  "query": "lungo",
    -  "location": <customer's location>
    -}
    -→ 200 OK
    -{
    -  "results": []
    -}
    -
    -

    This rule might be reduced to: if an array is the result of the operation, then the emptiness of that array is not a mistake, but a correct response. (Of course, if an empty array is acceptable semantically; an empty array of coordinates is a mistake for sure.)

    -
    22. Localization and internationalization
    +

    Often, making such offloads on an ad-hoc basis, e.g. bypassing the API, is a reasonable practice.

    +
    25. Localization and internationalization

    All endpoints must accept language parameters (for example, in a form of the Accept-Language header), even if they are not being used currently.

    It is important to understand that the user's language and the user's jurisdiction are different things. Your API working cycle must always store the user's location. It might be stated either explicitly (requests contain geographical coordinates) or implicitly (initial location-bound request initiates session creation which stores the location), but no correct localization is possible in absence of location data. In most cases reducing the location to just a country code is enough.

    The thing is that lots of parameters potentially affecting data formats depend not on language, but on a user's location. To name a few: number formatting (integer and fractional part delimiter, digit groups delimiter), date formatting, the first day of the week, keyboard layout, measurement units system (which might be non-decimal!), etc. In some situations, you need to store two locations: user residence location and user ‘viewport’. For example, if a US citizen is planning a European trip, it's convenient to show prices in local currency, but measure distances in miles and feet.

    Sometimes explicit location passing is not enough since there are lots of territorial conflicts in the world. How the API should behave when user coordinates lie within disputed regions is a legal matter, regretfully. The author of this book once had to implement a ‘state A territory according to state B official position’ concept.

    -

    Important: mark a difference between localization for end users and localization for developers. Take a look at the example in rule #19: localized_message is meant for the user; the app should show it if there is no specific handler for this error exists in code. This message must be written in the user's language and formatted according to the user's location. But the details.checks_failed[].message is meant to be read by developers examining the problem. So it must be written and formatted in a manner that suits developers best. In the software development world, it usually means ‘in English’.

    +

    Important: mark a difference between localization for end users and localization for developers. Take a look at the example in rule #12: the localized_message is meant for the user; the app should show it if there is no specific handler for this error exists in the code. This message must be written in the user's language and formatted according to the user's location. But the details.checks_failed[].message is meant to be read by developers examining the problem. So it must be written and formatted in a manner that suits developers best. In the software development world, it usually means ‘in English’.

    Worth mentioning is that the localized_ prefix in the example is used to differentiate messages to users from messages to developers. A concept like that must be, of course, explicitly stated in your API docs.

    And one more thing: all strings must be UTF-8, no exclusions.

    Chapter 12. Annex to Section I. Generic API Example

    Let's summarize the current state of our API study.

    @@ -2256,20 +2464,33 @@ POST /v1/orders { "results": [{ // Place data - "place": { "name", "location" }, + "place": + { "name", "location" }, // Coffee machine properties - "coffee-machine": { "id", "brand", "type" }, + "coffee-machine": + { "id", "brand", "type" }, // Route data - "route": { "distance", "duration", "location_tip" }, + "route": { + "distance", + "duration", + "location_tip" + }, "offers": [{ // Recipe data - "recipe": { "id", "name", "description" }, + "recipe": + { "id", "name", "description" }, // Recipe specific options - "options": { "volume" }, + "options": + { "volume" }, // Offer metadata - "offer": { "id", "valid_until" }, + "offer": + { "id", "valid_until" }, // Pricing - "pricing": { "currency_code", "price", "localized_price" }, + "pricing": { + "currency_code", + "price", + "localized_price" + }, "estimated_waiting_time" }, …] }, …], @@ -2286,7 +2507,11 @@ GET /v1/recipes?cursor=<cursor>
    // Returns the recipe by its id
     GET /v1/recipes/{id}
     →
    -{ "recipe_id", "name", "description" }
    +{ 
    +  "recipe_id", 
    +  "name", 
    +  "description" 
    +}
     
    3. Working with orders
    // Creates an order
    @@ -2362,7 +2587,11 @@ POST /v1/runs/{id}/cancel
     
    6. Managing runtimes
    // Creates a new runtime
     POST /v1/runtimes
    -{ "coffee_machine_id", "program_id", "parameters" }
    +{ 
    +  "coffee_machine_id", 
    +  "program_id", 
    +  "parameters" 
    +}
     →
     { "runtime_id", "state" }
     
    @@ -2469,7 +2698,19 @@ POST /v1/runtimes/{id}/terminate -

    We will address these questions in more detail in the next chapters. Additionally, in Section III we will also discuss, how to communicate to customers about new releases and discontinued supporting of older versions, and how to stimulate them to adopt new API versions.

    Chapter 14. On the Waterline of the Iceberg

    +

    We will address these questions in more detail in the next chapters. Additionally, in Section III we will also discuss, how to communicate to customers about new releases and discontinued supporting of older versions, and how to stimulate them to adopt new API versions.

    +

    Simultaneous access to several API versions

    +

    In modern professional software development, especially if we talk about internal APIs, a new API version usually fully replaces the previous one. If some problems are found, it might be rolled back (by releasing the previous version), but the two builds never co-exist. However, in the case of public APIs, the more the number of partner integrations is, the more dangerous this approach becomes.

    +

    Indeed, with the growth of the number of users, the ‘rollback the API version in case of problems’ paradigm becomes increasingly destructive. To a partner, the optimal solution is rigidly referencing the specific API version — the one that had been tested (ideally, at the same time having the API vendor somehow seamlessly fixing security issues and making their software compliant with newly introduced legislation).

    +

    NB. From the same considerations, providing beta (or maybe even alpha) versions of the popular APIs becomes more and more desirable as well, to make partners test the upcoming version and address the possible issues in advance.

    +

    The important (and undeniable) advantage of the semver system is that it provides the proper version granularity:

    +
      +
    • stating the first digit (major version) allows for getting a backwards-compatible version of the API;
    • +
    • stating two digits (major and minor versions) allows to guarantee that some functionality that was added after the initial release will be available;
    • +
    • finally, stating all three numbers (major version, minor version, and patch) allows for fixing a concrete API release with all its specificities (and errors), which — theoretically — means that the integration will remain operable till this version is physically available.
    • +
    +

    Of course, preserving minor versions infinitely isn't possible (partly because of security and compliance issues that tend to pile up). However, providing such access for a reasonable period of time is rather a hygienic norm for popular APIs.

    +

    NB. Sometimes to defend the single accessible API version concept, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backwards compatibility, as it might be relying on some un-versioned services (for example, some data in the DB that are shared between all the API versions). We, however, consider this an additional reason to isolate such dependencies (see ‘The Serenity Notepad’ chapter) as it means that changes to these subsystems might lead to the inoperability of the API.

    Chapter 14. On the Waterline of the Iceberg

    Before we start talking about the extensible API design, we should discuss the hygienic minimum. A huge number of problems would have never happened if API vendors had paid more attention to marking their area of responsibility.

    1. Provide a minimal amount of functionality

    At any moment in its lifetime, your API is like an iceberg: it comprises an observable (e.g. documented) part and a hidden one, undocumented. If the API is designed properly, these two parts correspond to each other just like the above-water and under-water parts of a real iceberg do, i.e. one to ten. Why so? Because of two obvious reasons.

    @@ -2507,7 +2748,8 @@ while (true) { try { status = api.getStatus(order.id); } catch (e) { - if (e.httpStatusCode != 404 || timeoutExceeded()) { + if (e.httpStatusCode != 404 || + timeoutExceeded()) { break; } } @@ -2531,11 +2773,16 @@ resolve();

    Of course, the developers of the language standard can afford such tricks; but you as an API developer cannot. You must at least document this behavior and make the signatures point to it; actually, good advice is to avoid such conventions, since they are simply unobvious while reading the code. And of course, under no circumstances, you can actually change this behavior to an asynchronous one.

    Example #3. Imagine you're providing animations API, which includes two independent functions:

    // Animates object's width,
    -// beginning with first value, ending with second
    +// beginning with first value, 
    +// ending with second
     // in a specified time period
    -object.animateWidth('100px', '500px', '1s');
    +object.animateWidth(
    +  '100px', '500px', '1s'
    +);
     // Observes object's width changes
    -object.observe('widthchange', observerFunction);
    +object.observe(
    +  'widthchange', observerFunction
    +);
     

    A question arises: how frequently and at what time fractions the observerFunction will be called? Let's assume in the first SDK version we emulated step-by-step animation at 10 frames per second: then the observerFunction will be called 10 times, getting values '140px', '180px', etc., up to '500px'. But then in a new API version, we switched to implementing both functions atop of a system's native functionality — and so you simply don't know, when and how frequently the observerFunction will be called.

    Just changing call frequency might result in making some code dysfunctional — for example, if the callback function makes some complex calculations, and no throttling is implemented since the developer just relied on your SDK's built-in throttling. And if the observerFunction ceases to be called when exactly '500px' is reached because of some system algorithms specifics, some code will be broken beyond any doubt.

    @@ -2543,26 +2790,25 @@ object.observe('widthchange', observerFunction);

    Example #4. Imagine that customer orders are passing through a specific pipeline:

    GET /v1/orders/{id}/events/history
     →
    -{
    -    "event_history": [
    -        {
    -            "iso_datetime": "2020-12-29T00:35:00+03:00",
    -            "new_status": "created"
    -        },
    -        {
    -            "iso_datetime": "2020-12-29T00:35:10+03:00",
    -            "new_status": "payment_approved"
    -        },
    -        {
    -            "iso_datetime": "2020-12-29T00:35:20+03:00",
    -            "new_status": "preparing_started"
    -        },
    -        {
    -            "iso_datetime": "2020-12-29T00:35:30+03:00",
    -            "new_status": "ready"
    -        }
    -    ]
    -}
    +{ "event_history": [
    +  {
    +    "iso_datetime": 
    +      "2020-12-29T00:35:00+03:00",
    +    "new_status": "created"
    +  }, {
    +    "iso_datetime": 
    +      "2020-12-29T00:35:10+03:00",
    +    "new_status": "payment_approved"
    +  }, {
    +    "iso_datetime": 
    +      "2020-12-29T00:35:20+03:00",
    +    "new_status": "preparing_started"
    +  }, {
    +    "iso_datetime": 
    +      "2020-12-29T00:35:30+03:00",
    +    "new_status": "ready"
    +  }
    +]}
     

    Suppose at some moment we decided to allow trustworthy clients to get their coffee in advance before the payment is confirmed. So an order will jump straight to "preparing_started", or event "ready", without a "payment_approved" event being emitted. It might appear to you that this modification is backwards-compatible since you've never really promised any specific event order being maintained, but it is not.

    Let's assume that a developer (probably, your company's business partner) wrote some code implementing some valuable business procedure, for example, gathering income and expenses analytics. It's quite logical to expect this code operates a state machine, which switches from one state to another depending on getting (or getting not) specific events. This analytical code will be broken if the event order changes. In the best-case scenario, a developer will get some exceptions and have to cope with the error's cause; the worst-case, partners will operate wrong statistics for an indefinite period of time until they find a mistake.

    @@ -2631,7 +2877,8 @@ PUT /v1/partners/{partnerId}/coffee-machines
  • Add new ‘with-options’ endpoint:

    -
    PUT /v1/partners/{partner_id}/coffee-machines-with-options
    +
    PUT /v1/partners/{partner_id}⮠
    +  /coffee-machines-with-options
     {
       "coffee_machines": [{
         "id",
    @@ -2698,9 +2945,15 @@ POST /v1/recipes
     

    The rule of contexts

    To make things worse, let us state that the inverse principle is actually correct either: high-level entities must not define low-level ones, since that simply isn't their responsibility. The exit from this logical labyrinth is that high-level entities must define a context, which other objects are to interpret. To properly design adding a new recipe interface we shouldn't try to find a better data format; we need to understand what contexts, both explicit and implicit, exist in our subject area.

    We have already found a localization context. There is some set of languages and regions we support in our API, and there are requirements — what exactly the partner must provide to make our API work in a new region. More specifically, there must be some formatting function to represent beverage volume somewhere in our API code:

    -
    l10n.volume.format(value, language_code, country_code)
    -// l10n.formatVolume('300ml', 'en', 'UK') → '300 ml'
    -// l10n.formatVolume('300ml', 'en', 'US') → '10 fl oz'
    +
    l10n.volume.format(
    +  value, language_code, country_code
    +)
    +// l10n.formatVolume(
    +//   '300ml', 'en', 'UK'
    +// ) → '300 ml'
    +// l10n.formatVolume(
    +//   '300ml', 'en', 'US'
    +// ) → '10 fl oz'
     

    To make our API work correctly with a new language or region, the partner must either define this function or point which pre-existing implementation to use. Like this:

    // Add a general formatting rule
    @@ -2792,8 +3045,14 @@ PUT /formatters/volume/ru/US
       // Add all the formatters needed
       "formatters": {
         "volume": [
    -      { "language_code", "template" },
    -      { "language_code", "country_code", "template" }
    +      { 
    +        "language_code", 
    +        "template" 
    +      }, { 
    +        "language_code", 
    +        "country_code", 
    +        "template" 
    +      }
         ]
       },
       // Other actions needed to be done
    @@ -2813,7 +3072,8 @@ PUT /formatters/volume/ru/US
     }
     →
     {
    -  "id": "my-coffee-company:lungo-customato"
    +  "id": 
    +    "my-coffee-company:lungo-customato"
     }
     

    Also note that this format allows us to maintain an important extensibility point: different partners might have totally isolated namespaces, or conversely share them. Furthermore, we might introduce special namespaces (like ‘common’, for example) to allow for publishing new recipes for everyone (and that, by the way, would allow us to organize our own backoffice to edit recipes).

    Chapter 17. Weak Coupling

    @@ -2892,21 +3152,28 @@ PUT /partners/{id}/coffee-machines

    There are different techniques to organize this data flow, but, basically, we always have two context descriptions and a two-way event stream in-between. If we were developing an SDK we would express the idea like this:

    /* Partner's implementation of the program
        run procedure for a custom API type */
    -registerProgramRunHandler(apiType, (program) => {
    -  // Initiating an execution
    -  // on partner's side
    -  let execution = initExecution(…);
    -  // Listen to parent context's changes
    -  program.context.on('takeout_requested', () => {
    -    // If takeout is requested, initiate
    -    // corresponding procedures
    -    execution.prepareTakeout(() => {
    -      // When the cup is ready for takeout,
    -      // emit corresponding event
    -      // for higher-level entity to catch it
    -      execution.context.emit('takeout_ready');
    -    });
    -  });
    +registerProgramRunHandler(
    +  apiType, 
    +  (program) => {
    +    // Initiating an execution
    +    // on partner's side
    +    let execution = initExecution(…);
    +    // Listen to parent context's changes
    +    program.context.on(
    +      'takeout_requested', 
    +      () => {
    +        // If takeout is requested, initiate
    +        // corresponding procedures
    +        execution.prepareTakeout(() => {
    +          // When the cup is ready for takeout,
    +          // emit corresponding event for 
    +          // a higher-level entity to catch it
    +          execution.context
    +            .emit('takeout_ready');
    +        }
    +      );
    +    }
    +  );
     
       return execution.context;
     });
    @@ -2931,30 +3198,38 @@ registerProgramRunHandler(apiType, (program) => {
     

    It becomes obvious from what was said above that two-way weak coupling means a significant increase in code complexity on both levels, which is often redundant. In many cases, two-way event linking might be replaced with one-way linking without significant loss of design quality. That means allowing a low-level entity to call higher-level methods directly instead of generating events. Let's alter our example:

    /* Partner's implementation of program
        run procedure for a custom API type */
    -registerProgramRunHandler(apiType, (program) => {
    -  // Initiating an execution
    -  // on partner's side
    -  let execution = initExecution(…);
    -  // Listen to parent context's changes
    -  program.context.on('takeout_requested', () => {
    -    // If takeout is requested, initiate
    -    // corresponding procedures
    -    execution.prepareTakeout(() => {
    -      /* When the order is ready for takeout,
    -         signalize about that, but not
    -         with event emitting */
    -      // execution.context.emit('takeout_ready')
    -      program.context.set('takeout_ready');
    -      // Or even more rigidly
    -      // program.setTakeoutReady();
    +registerProgramRunHandler(
    +  apiType, 
    +  (program) => {
    +    // Initiating an execution
    +    // on partner's side
    +    let execution = initExecution(…);
    +    // Listen to parent context's changes
    +    program.context.on(
    +      'takeout_requested', 
    +      () => {
    +        // If takeout is requested, initiate
    +        // corresponding procedures
    +        execution.prepareTakeout(() => {
    +          /* When the order is ready 
    +             for takeout, signalize about that
    +             by calling a method, not
    +             with event emitting */
    +          // execution.context
    +          //   .emit('takeout_ready')
    +          program.context
    +            .set('takeout_ready');
    +          // Or even more rigidly
    +          // program.setTakeoutReady();
    +        }
    +      );
         });
    -  });
    -  /* Since we're modifying parent context
    -     instead of emitting events, we don't 
    -     actually need to return anything */
    -  // return execution.context;
    -});
    -}
    +    /* Since we're modifying parent context
    +      instead of emitting events, we don't 
    +      actually need to return anything */
    +    // return execution.context;
    +  }
    +);
     

    Again, this solution might look counter-intuitive, since we efficiently returned to strong coupling via strictly defined methods. But there is an important difference: we're making all this stuff up because we expect alternative implementations of the lower abstraction level. Situations with different realizations of higher abstraction levels emerging are, of course, possible, but quite rare. The tree of alternative implementations usually grows from top to bottom.

    Another reason to justify this solution is that major changes occurring at different abstraction levels have different weights:

    @@ -3446,7 +3721,8 @@ ProgramContext.dispatch = (action) => {

    Additional means of tracking are users' unique identifiers, most notably cookies. However, most recently this method of gathering data is under attack from several sides: browser makers restrict third-party cookies, users are employing anti-tracker software, and lawmakers started to roll out legal requirements against data collection. In the current situation, it's much easier to drop cookie usage than to be compliant with all the regulations.

    All this leads to a situation when public APIs (especially those installed on free-to-use sites and applications) are very limited in the means of collecting the statistics and analyzing user behavior. And that impacts not only fighting all kinds of fraud but analyzing use cases as well. That's the way.

  • -

    Chapter 28. The Technical Means of Preventing ToS Violations

    + +

    NB. In some jurisdictions, IP addresses are considered personal data, and collecting them is prohibited as well. We don't dare to advise on how an API vendor might at the same time be able to fight prohibited content on the platform and don't have access to users' IP addresses. We presume that complying with such legislation implies storing statistics by IP address hashes. (And just in case we won't mention that building a rainbow table for SHA-256 hashes covering the entire 4-billion range of IPv4 addresses would take several hours on a regular office-grade computer.)

    Chapter 28. The Technical Means of Preventing ToS Violations

    Implementing the paradigm of a centralized system of preventing partner endpoints-bound fraud, which we described in the previous chapter, in practice faces non-trivial difficulties.

    The task of filtering out illicit API requests generally comprises three steps:

      @@ -3573,7 +3849,7 @@ ProgramContext.dispatch = (action) => {

      Before we start describing documentation types and formats, we should stress one important statement: developers interact with your help articles totally unlike you expect them to. Remember yourself working on the project: you make quite specific actions.

      1. First, you need to determine whether this service covers your needs in general (as quickly as possible);
      2. -
      3. If yes, you look for specific functionality to resolve your specific case.
      4. +
      5. If it does, you look for specific functionality to resolve your specific case.

      In fact, newcomers (e.g. those developers who are not familiar with the API) usually want just one thing: to assemble the code that solves their problem out of existing code samples and never return to this issue again. Sounds not exactly reassuringly, given the amount of work invested into the API and its documentation development, but that's what the reality looks like. Also, that's the root cause of developers' dissatisfaction with the docs: it's literally impossible to have articles covering exactly that problem the developer comes with being detailed exactly to the extent the developer knows the API concepts. In addition, non-newcomers (e.g. those developers who have already learned the basics concepts and are now trying to solve some advanced problems) do not need these ‘mixed examples’ articles as they look for some deeper understanding.

      Introductory notes

      diff --git a/src/en/clean-copy/02-Section I. The API Design/03.md b/src/en/clean-copy/02-Section I. The API Design/03.md index 3d4b667..1c43f6f 100644 --- a/src/en/clean-copy/02-Section I. The API Design/03.md +++ b/src/en/clean-copy/02-Section I. The API Design/03.md @@ -101,7 +101,8 @@ A naïve approach to this situation is to design an interim abstraction level as "status": "executing", "operations": [ // description of commands - // being executed on a physical coffee machine + // being executed on + // a physical coffee machine ] } … @@ -131,7 +132,8 @@ To be more specific, let's assume those two kinds of coffee machines provide the } ``` ``` - // Starts an execution of a specified program + // Starts an execution + // of a specified program // and returns execution status POST /execute { @@ -154,7 +156,8 @@ To be more specific, let's assume those two kinds of coffee machines provide the ``` ``` // Returns execution status. - // The format is the same as in `POST /execute` + // The format is the same + // as in the `POST /execute` method GET /execution/status ``` @@ -174,9 +177,13 @@ To be more specific, let's assume those two kinds of coffee machines provide the // * pour_water // * discard_cup "type": "set_cup", - // Arguments available to each operation. - // To keep it simple, let's limit these to one: - // * volume — a volume of a cup, coffee, or water + // Arguments available + // to each operation. + // To keep it simple, + // let's limit these to one: + // * volume + // — a volume of a cup, + // coffee, or water "arguments": ["volume"] }, … @@ -189,7 +196,10 @@ To be more specific, let's assume those two kinds of coffee machines provide the POST /functions { "type": "set_cup", - "arguments": [{ "name": "volume", "value": "300ml" }] + "arguments": [{ + "name": "volume", + "value": "300ml" + }] } ``` ``` @@ -276,7 +286,7 @@ POST /v1/programs/{id}/run Please note that knowing the coffee machine API kind isn't required at all; that's why we're making abstractions! We could possibly make interfaces more specific, implementing different `run` and `match` endpoints for different coffee machines: * `POST /v1/program-matcher/{api_type}` - * `POST /v1/programs/{api_type}/{program_id}/run` + * `POST /v1/{api_type}/programs/{id}/run` This approach has some benefits, like the possibility to provide different sets of parameters, specific to the API kind. But we see no need for such fragmentation. `run` method handler is capable of extracting all the program metadata and performing one of two actions: * call `POST /execute` physical API method, passing internal program identifier — for the first API kind; @@ -286,7 +296,11 @@ Out of general concerns runtime level for the second-kind API will be private, s ``` POST /v1/runtimes -{ "coffee_machine", "program", "parameters" } +{ + "coffee_machine", + "program", + "parameters" +} → { "runtime_id", "state" } ``` @@ -315,14 +329,20 @@ And the `state` like that: // * "finished" — all operations done "status": "ready_waiting", // Command being currently executed. - // Similar to line numbers in computer programs + // Similar to line numbers + // in computer programs "command_sequence_id", // How the execution concluded: - // * "success" — beverage prepared and taken - // * "terminated" — execution aborted - // * "technical_error" — preparation error - // * "waiting_time_exceeded" — beverage prepared, - // but not taken; timed out then disposed + // * "success" + // — beverage prepared and taken + // * "terminated" + // — execution aborted + // * "technical_error" + // — preparation error + // * "waiting_time_exceeded" + // — beverage prepared, + // but not taken; + // timed out then disposed "resolution": "success", // All variables values, // including sensors state diff --git a/src/en/clean-copy/02-Section I. The API Design/04.md b/src/en/clean-copy/02-Section I. The API Design/04.md index ec9af0c..0595f94 100644 --- a/src/en/clean-copy/02-Section I. The API Design/04.md +++ b/src/en/clean-copy/02-Section I. The API Design/04.md @@ -47,16 +47,25 @@ Obviously, the first step is offering a choice to a user, to make them point out If we try writing pseudocode, we will get something like that: ``` // Retrieve all possible recipes -let recipes = api.getRecipes(); -// Retrieve a list of all available coffee machines -let coffeeMachines = api.getCoffeeMachines(); +let recipes = + api.getRecipes(); +// Retrieve a list of +// all available coffee machines +let coffeeMachines = + api.getCoffeeMachines(); // Build a spatial index -let coffeeMachineRecipesIndex = buildGeoIndex(recipes, coffeeMachines); -// Select coffee machines matching user's needs -let matchingCoffeeMachines = coffeeMachineRecipesIndex.query( - parameters, - { "sort_by": "distance" } -); +let coffeeMachineRecipesIndex = + buildGeoIndex( + recipes, + coffeeMachines + ); +// Select coffee machines +// matching user's needs +let matchingCoffeeMachines = + coffeeMachineRecipesIndex.query( + parameters, + { "sort_by": "distance" } + ); // Finally, show offers to user app.display(coffeeMachines); ``` @@ -82,7 +91,12 @@ POST /v1/offers/search → { "results": [ - { "coffee_machine", "place", "distance", "offer" } + { + "coffee_machine", + "place", + "distance", + "offer" + } ], "cursor" } @@ -119,12 +133,15 @@ One solution is to provide a special identifier to an offer. This identifier mus { "results": [ { - "coffee_machine", "place", "distance", + "coffee_machine", + "place", + "distance", "offer": { "id", "price", "currency_code", - // Date and time when the offer expires + // Date and time + // when the offer expires "valid_until" } } @@ -142,11 +159,9 @@ And one more step towards making developers' life easier: how an ‘invalid pric ``` POST /v1/orders -{ … "offer_id" …} +{ "offer_id", … } → 409 Conflict -{ - "message": "Invalid price" -} +{ "message": "Invalid price" } ``` Formally speaking, this error response is enough: users get the ‘Invalid price’ message, and they have to repeat the order. But from a UX point of view that would be a horrible decision: the user hasn't made any mistakes, and this message isn't helpful at all. @@ -172,7 +187,8 @@ In our case, the price mismatch error should look like this: // Error kind "reason": "offer_invalid", "localized_message": - "Something goes wrong. Try restarting the app." + "Something goes wrong.⮠ + Try restarting the app." "details": { // What's wrong exactly? // Which validity checks failed? @@ -198,40 +214,37 @@ The only possible method of overcoming this law is decomposition. Entities shoul Let's take a look at a simple example: what the coffee machine search function returns. To ensure an adequate UX of the app, quite bulky datasets are required. ``` { - "results": [ - { - "coffee_machine_id", - "coffee_machine_type": "drip_coffee_maker", - "coffee_machine_brand", - "place_name": "The Chamomile", - // Coordinates of a place - "place_location_latitude", - "place_location_longitude", - "place_open_now", - "working_hours", - // Walking route parameters - "walking_distance", - "walking_time", - // How to find the place - "place_location_tip", - "offers": [ - { - "recipe": "lungo", - "recipe_name": "Our brand new Lungo®™", - "recipe_description", - "volume": "800ml", - "offer_id", - "offer_valid_until", - "localized_price": "Just $19 for a large coffee cup", - "price": "19.00", - "currency_code": "USD", - "estimated_waiting_time": "20s" - }, - … - ] - }, - … - ] + "results": [{ + "coffee_machine_id", + "coffee_machine_type": + "drip_coffee_maker", + "coffee_machine_brand", + "place_name": "The Chamomile", + // Coordinates of a place + "place_location_latitude", + "place_location_longitude", + "place_open_now", + "working_hours", + // Walking route parameters + "walking_distance", + "walking_time", + // How to find the place + "place_location_tip", + "offers": [{ + "recipe": "lungo", + "recipe_name": + "Our brand new Lungo®™", + "recipe_description", + "volume": "800ml", + "offer_id", + "offer_valid_until", + "localized_price": + "Just $19 for a large coffee cup", + "price": "19.00", + "currency_code": "USD", + "estimated_waiting_time": "20s" + }, …] + }, …] } ``` @@ -256,16 +269,30 @@ Let's try to group it together: // Coffee machine properties "coffee-machine": { "id", "brand", "type" }, // Route data - "route": { "distance", "duration", "location_tip" }, + "route": { + "distance", + "duration", + "location_tip" + }, "offers": [{ // Recipe data - "recipe": { "id", "name", "description" }, + "recipe": { + "id", + "name", + "description" + }, // Recipe specific options - "options": { "volume" }, + "options": + { "volume" }, // Offer metadata - "offer": { "id", "valid_until" }, + "offer": + { "id", "valid_until" }, // Pricing - "pricing": { "currency_code", "price", "localized_price" }, + "pricing": { + "currency_code", + "price", + "localized_price" + }, "estimated_waiting_time" }, …] }, …] diff --git a/src/en/clean-copy/02-Section I. The API Design/05.md b/src/en/clean-copy/02-Section I. The API Design/05.md index dedecfe..c699994 100644 --- a/src/en/clean-copy/02-Section I. The API Design/05.md +++ b/src/en/clean-copy/02-Section I. The API Design/05.md @@ -16,7 +16,7 @@ It is important to understand that you always can introduce concepts of your own #### Ensuring readability and consistency -The most important task for the API vendor is to make code written atop of the API by third-party developers easily readable and maintainable. Remember, that the law of large numbers works against you: if some concept or a signature might be treated wrong, they will be inevitably treated wrong by a number of partners, and this number will be increasing with the API popularity growth. +The most important task for the API vendor is to make code written by third-party developers atop of the API easily readable and maintainable. Remember that the law of large numbers works against you: if some concept or a signature might be treated wrong, they will be inevitably treated wrong by a number of partners, and this number will be increasing with the API popularity growth. ##### Explicit is always better than implicit @@ -142,7 +142,8 @@ If an entity name is a polysemantic term itself, which could confuse developers, **Bad**: ``` -// Returns a list of coffee machine builtin functions +// Returns a list of +// coffee machine builtin functions GET /coffee-machines/{id}/functions ``` Word ‘function’ is many-valued. It could mean built-in functions, but also ‘a piece of code’, or a state (machine is functioning). @@ -164,7 +165,8 @@ strpos(haystack, needle) ``` ``` // Replace all occurrences -// of the search string with the replacement string +// of the search string +// with the replacement string str_replace(needle, replace, haystack) ``` Several rules are violated: @@ -217,8 +219,12 @@ POST /v1/orders This new `contactless_delivery` option isn't required, but its default value is `true`. A question arises: how developers should discern explicit intention to abolish the option (`false`) from knowing not it exists (field isn't set). They have to write something like: ``` -if (Type(order.contactless_delivery) == 'Boolean' && - order.contactless_delivery == false) { … } +if (Type( + order.contactless_delivery + ) == 'Boolean' && + order.contactless_delivery == false) { + … +} ``` This practice makes the code more complicated, and it's quite easy to make mistakes, which will effectively treat the field in an opposite manner. The same could happen if some special values (i.e. `null` or `-1`) to denote value absence are used. @@ -348,7 +354,8 @@ POST /v1/coffee-machines/search }, { "field": "position.latitude", - "error_type": "constraint_violation", + "error_type": + "constraint_violation", "constraints": { "min": -90, "max": 90 @@ -476,7 +483,7 @@ You may note that in this setup the error can't be resolved in one step: this si #### Developing machine-readable interfaces -In pursuit of the API clarity for humans, we frequently forget that it's not developers themselves who interact with the endpoints, but the code they've written. Many concepts that work well with the user interface, are badly suited for the program ones: specifically, developers can't make decisions based on textual information, and they can't ‘refresh’ the state in case of some confusing situation. +In pursuit of the API clarity for humans, we frequently forget that it's not developers themselves who interact with the endpoints, but the code they've written. Many concepts that work well with user interfaces, are badly suited for the program ones: specifically, developers can't make decisions based on textual information, and they can't ‘refresh’ the state in case of some confusing situation. ##### The system state must be observable by clients @@ -639,12 +646,14 @@ At the first glance, this is the most standard way of organizing the pagination // sorted by creation date // starting with a record with an identifier // following the specified one -GET /v1/records?older_than={record_id}&limit=10 +GET /v1/records⮠ + ?older_than={record_id}&limit=10 // Returns a limited number of records // sorted by creation date // starting with a record with an identifier // preceding the specified one -GET /v1/records?newer_than={record_id}&limit=10 +GET /v1/records⮠ + ?newer_than={record_id}&limit=10 ``` With the pagination organized like that, clients never bother about records being added or removed in the processed part of the list: they continue to iterate over the records, either getting new ones (using `newer_than`) or older ones (using `older_than`). If there is no record removal operation, clients may easily cache responses — the URL will always return the same record set. @@ -686,7 +695,8 @@ There are several approaches to implementing cursors (for example, making a sing **Bad**: ``` // Returns a limited number of records -// sorted by a specified field in a specified order +// sorted by a specified field +// in a specified order // starting with a record with an index // equals to `offset` GET /records?sort_by=date_modified⮠ @@ -703,9 +713,10 @@ Sorting by the date of modification usually means that data might be modified. I // Creates a view based on the parameters passed POST /v1/record-views { - sort_by: [ - { "field": "date_modified", "order": "desc" } - ] + sort_by: [{ + "field": "date_modified", + "order": "desc" + }] } → { "id", "cursor" } @@ -713,7 +724,8 @@ POST /v1/record-views ``` // Returns a portion of the view -GET /v1/record-views/{id}?cursor={cursor} +GET /v1/record-views/{id}⮠ + ?cursor={cursor} ``` Since the produced view is immutable, access to it might be organized in any form, including a limit-offset scheme, cursors, `Range` header, etc. However, there is a downside: records modified after the view was generated will be misplaced or outdated. @@ -747,7 +759,7 @@ If the protocol allows, fractional numbers with fixed precision (like money sums 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. -If conversion to float number will certainly lead to losing the precision (let's say if we translate ‘20 minutes’ into hours as a decimal fraction), it's better to either stick to a fully precise format (e.g. opt for `00:20` instead of `0.33333…`) or to provide an SDK to work with this data, or as a last resort describe the rounding principles in the documentation. +If conversion to a float number will certainly lead to losing the precision (let's say if we translate ‘20 minutes’ into hours as a decimal fraction), it's better to either stick to a fully precise format (e.g. opt for `00:20` instead of `0.33333…`) or to provide an SDK to work with this data, or as a last resort describe the rounding principles in the documentation. ##### All API operations must be idempotent @@ -989,9 +1001,9 @@ Just in case: nested operations must be idempotent themselves. If they are not, If the author of this book was given a dollar each time he had to implement the additional security protocol invented by someone, he would already retire. The API developers' passion for signing request parameters or introducing complex schemes of exchanging passwords for tokens is as obvious as meaningless. -**First**, almost all security-enhancing procedures for every kind of operation *are already developed*. There is no need to re-thinking them anew, just take the existing approach and implement it. No self-invented algorithm for request signature checking provides the same level of preventing [Man-in-the-Middle attack](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) as a TLS connection with mutual certificate pinning. +**First**, almost all security-enhancing procedures for every kind of operation *are already invented*. There is no need to re-think them anew; just take the existing approach and implement it. No self-invented algorithm for request signature checking provides the same level of preventing [Man-in-the-Middle attack](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) as a TLS connection with mutual certificate pinning. -**Second**, it's quite presumptuously (and dangerous) to assume you're an expert in security. New attack vectors come every day, and being aware of all the actual threats is a full-day job. If you do something different during workdays, the security system designed by you will contain vulnerabilities that you have never heard about — for example, your password-checking algorithm might be susceptible to the [timing attack]((https://en.wikipedia.org/wiki/Timing_attack), and your web-server, to the [request splitting attack](https://capec.mitre.org/data/definitions/105.html). +**Second**, it's quite presumptuous (and dangerous) to assume you're an expert in security. New attack vectors come every day, and being aware of all the actual threats is a full-day job. If you do something different during workdays, the security system designed by you will contain vulnerabilities that you have never heard about — for example, your password-checking algorithm might be susceptible to the [timing attack](https://en.wikipedia.org/wiki/Timing_attack), and your web-server, to the [request splitting attack](https://capec.mitre.org/data/definitions/105.html). ##### Explicitly declare technical restrictions @@ -1014,14 +1026,14 @@ If the first two problems are solved by applying pure technical measures (see th * do not rely too heavily on asynchronous interfaces; * on one side, they allow tackling many technical problems related to the API performance, which, in turn, allows for maintaining backwards compatibility: if some method is asynchronous from the very beginning, the latencies and the data consistency models might be easily tuned if needed; - * from the other side, the number of requests clients generate becomes hardly predicable, as a client needs to make some number of attempts to get the result which might not be known in advance; + * from the other side, the number of requests clients generate becomes hardly predicable, as a client in order to retrieve a result needs to make some unpredictable number of attempts; * declare an explicit retry policy (for example, with the `Retry-After` header); * yes, some partners will ignore it as developers will get too lazy to implement it, but some will not (especially if you provide the SDKs as well); * if you expect a significant number of asynchronous operations in the API, allow developers to choose between the poll model (clients make repeated requests to an endpoint to check the asynchronous procedure status) and the push model (the server notifies clients of status changes, for example, via webhooks or server-push mechanics); - * if some entity comprises both ‘lightweight’ data (let's say, the name and the description of the recipe) and ‘heavy’ data (let's say, the promo picture of the beverage which might easily be a hundred times larger than text fields) it's better to split endpoints and pass only a reference to the ‘heavy’ data (a link to the image, in our case) — this will allow at least setting different cache policies for different kinds of data. + * if some entity comprises both ‘lightweight’ data (let's say, the name and the description of the recipe) and ‘heavy’ data (let's say, the promo picture of the beverage which might easily be a hundred times larger than the text fields), it's better to split endpoints and pass only a reference to the ‘heavy’ data (a link to the image, in our case) — this will allow at least setting different cache policies for different kinds of data. As a useful exercise, try modeling the typical lifecycle of a partner's app's main functionality (for example, making a single order) to count the number of requests and the amount of traffic that it takes. @@ -1063,20 +1075,20 @@ PATCH /v1/orders/{id} This signature is bad per se as it's unreadable. What does `null` as the first array element mean — is it a deletion of an element or an indication that no actions are needed towards it? What happens with the fields that are not stated in the update operation body (`delivery_address`, `milk_type`) — will they be reset to defaults, or stay unchanged? -The nastiest part is that whatever option you choose, the number of problems will only multiply further. Let's say we agreed that the `{ "items":[null, {…}] }` statement means that the first element of the array is left untouched, e.g. no changes are needed. Then, how shall we encode its deletion? Invent one more ‘magical’ value meaning ‘remove it’? Similarly, if the fields that are not explicitly mentioned retain their value — how to reset them to defaults? +The nastiest part is that whatever option you choose, the number of problems will only multiply further. Let's say we agreed that the `{"items":[null, {…}]}` statement means that the first element of the array is left untouched, e.g. no changes are needed. Then, how shall we encode its deletion? Invent one more ‘magical’ value meaning ‘remove it’? Similarly, if the fields that are not explicitly mentioned retain their value — how to reset them to defaults? **The simple solution** is always rewriting the data entirely, e.g. to require passing the entire object, to replace the current state with it, and to return the full state as a result of the operation. This obvious solution is frequently rejected with the following reasoning: * increased requests sizes and therefore, the amount of traffic; * the necessity to detect which fields are changed (for instance, to generate proper state change events for subscribers); - * the inability of cooperative editing when two clients are editing different object properties simultaneously. + * the inability of organizing cooperative editing when two clients are editing different object properties simultaneously. -However, if we take a deeper look, all three disadvantages are actually imaginative: +However, if we take a deeper look, all these disadvantages are actually imaginative: * the reasons for increasing the amount of traffic were described in the previous paragraphs, and serving extra fields is not one of them (and if it is, it's rather a rationale to decompose the endpoint); * the concept of sending only those fields that changed is in fact about shifting the responsibility of change detection to clients; * it doesn't make the task any easier, and also introduces the problem of client code fragmentation as several independent implementations of the change detection algorithm will occur; * furthermore, the existence of the client algorithm for finding the fields that changed doesn't mean that the server might skip implementing it as client developers might make mistakes or simply spare the effort and always send all the fields; - * finally, this naïve approach to organizing collaborative editing works only with transitive changes (e.g. the final result does not depend on the order in which the operations were executed), and in our case, it's already not true: deletion of the first element and editing the second element are non-transitive; - * often, in addition to sparing traffic on requests, the same concept is applied to responses as well, returning empty bodies for modifying operations; thus two clients making simultaneous edits do not see one another's changes. + * finally, this naïve approach to organizing collaborative editing works only with transitive changes (e.g. if the final result does not depend on the order in which the operations were executed), and in our case, it's already not true: deletion of the first element and editing the second element are non-transitive; + * often, in addition to sparing traffic on requests, the same concept is applied to responses as well, e.g. no data is returned for modifying operations; thus two clients making simultaneous edits do not see one another's changes. **Better**: split the functionality. This also correlates well with the [decomposition principle](#chapter-10) we've discussed in the previous chapter. @@ -1137,13 +1149,13 @@ PUT /v1/orders/{id}/items/{item_id} DELETE /v1/orders/{id}/items/{item_id} ``` -Now to reset `volume` to its default value it's enough to omit it in the `PUT /items/{item_id}` request body. Also, the operations of deleting one item while simultaneously modifying another one are now idempotent. +Now to reset `volume` to its default value it's enough to omit it in the `PUT /items/{item_id}` request body. Also, the operations of deleting one item while simultaneously modifying another one are now transitive. This approach also allows for separating non-mutable and calculated fields (in our case, `created_at` and `status`) from editable ones without creating ambiguous situations (what should happen if a client tries to change the `created_at` field?) It is also possible to return full order objects from `PUT` endpoints instead of just the sub-resource that was overwritten (though it requires some naming convention). -**NB**: while decomposing endpoints, the idea of splitting them into mutable and non-mutable data often looks tempting. Then it's possible to make the latter infinitely cacheable and never bother with pagination ordering and update format consistency. The plan looks solid on paper, but with the API expansion, it frequently happens that immutable fields eventually cease being immutable, and the entire concept not only stops working properly but even starts looking like a design flaw. We would rather recommend designating data as immutable in one of the two cases: (1) making them editable will really mean breaking backwards compatibility, or (2) the link to the resource (for example, an image) is served via the API as well, and you do possess the capability of making those links persistent (e.g. you might generate a new link to the image instead of rewriting the contents of the old one). +**NB**: while decomposing endpoints, the idea of splitting them into mutable and non-mutable data often looks tempting. It makes possible to mark the latter as infinitely cacheable and never bother about pagination ordering and update format consistency. The plan looks solid on paper, but with the API expansion, it frequently happens that immutable fields eventually cease being immutable, and the entire concept not only stops working properly but even starts looking like a design flaw. We would rather recommend designating data as immutable in one of the two cases: (1) making them editable will really mean breaking backwards compatibility, or (2) the link to the resource (for example, an image) is served via the API as well, and you do possess the capability of making those links persistent (e.g. you might generate a new link to the image instead of rewriting the contents of the old one). **Even better**: design a format for atomic changes. @@ -1182,11 +1194,11 @@ One important implication: **never use increasing numbers as external identifier ##### Stipulate future restrictions -With the API popularity growth, it will inevitably become necessary to introduce technical means of preventing illicit API usage, such as displaying captcha, setting honeypots, raising the ‘too many requests’ exceptions, installing anti-DDoS proxies, etc. All these things cannot be done if the corresponding errors and messages were not described in the docs from the very beginning. +With the API popularity growth, it will inevitably become necessary to introduce technical means of preventing illicit API usage, such as displaying captchas, setting honeypots, raising the ‘too many requests’ exceptions, installing anti-DDoS proxies, etc. All these things cannot be done if the corresponding errors and messages were not described in the docs from the very beginning. You are not obliged to actually generate those exceptions, but you might stipulate this possibility in the terms of service. For example, you might describe the `429 Too Many Requests` error or captcha redirect, but implement the functionality when it's actually needed. -It is extremely important to leave room for multi-factored authentication (such as TOTP, SMS, or 3D-secure-like technologies) in case it's possible to make payments through the API. In this case, it's a must-have from the very beginning. +It is extremely important to leave room for multi-factored authentication (such as TOTP, SMS, or 3D-secure-like technologies) if it's possible to make payments through the API. In this case, it's a must-have from the very beginning. ##### Don't provide endpoints for mass downloading of sensitive data diff --git a/src/en/clean-copy/02-Section I. The API Design/06.md b/src/en/clean-copy/02-Section I. The API Design/06.md index 6d6fe67..4161ba0 100644 --- a/src/en/clean-copy/02-Section I. The API Design/06.md +++ b/src/en/clean-copy/02-Section I. The API Design/06.md @@ -18,20 +18,33 @@ POST /v1/offers/search { "results": [{ // Place data - "place": { "name", "location" }, + "place": + { "name", "location" }, // Coffee machine properties - "coffee-machine": { "id", "brand", "type" }, + "coffee-machine": + { "id", "brand", "type" }, // Route data - "route": { "distance", "duration", "location_tip" }, + "route": { + "distance", + "duration", + "location_tip" + }, "offers": [{ // Recipe data - "recipe": { "id", "name", "description" }, + "recipe": + { "id", "name", "description" }, // Recipe specific options - "options": { "volume" }, + "options": + { "volume" }, // Offer metadata - "offer": { "id", "valid_until" }, + "offer": + { "id", "valid_until" }, // Pricing - "pricing": { "currency_code", "price", "localized_price" }, + "pricing": { + "currency_code", + "price", + "localized_price" + }, "estimated_waiting_time" }, …] }, …], @@ -52,7 +65,11 @@ GET /v1/recipes?cursor= // Returns the recipe by its id GET /v1/recipes/{id} → -{ "recipe_id", "name", "description" } +{ + "recipe_id", + "name", + "description" +} ``` ##### Working with orders @@ -144,7 +161,11 @@ POST /v1/runs/{id}/cancel ``` // Creates a new runtime POST /v1/runtimes -{ "coffee_machine_id", "program_id", "parameters" } +{ + "coffee_machine_id", + "program_id", + "parameters" +} → { "runtime_id", "state" } ``` diff --git a/src/en/clean-copy/03-Section II. The Backwards Compatibility/02.md b/src/en/clean-copy/03-Section II. The Backwards Compatibility/02.md index 57d6e89..971121c 100644 --- a/src/en/clean-copy/03-Section II. The Backwards Compatibility/02.md +++ b/src/en/clean-copy/03-Section II. The Backwards Compatibility/02.md @@ -83,11 +83,16 @@ Of course, the developers of the language standard can afford such tricks; but y ``` // Animates object's width, -// beginning with first value, ending with second +// beginning with first value, +// ending with second // in a specified time period -object.animateWidth('100px', '500px', '1s'); +object.animateWidth( + '100px', '500px', '1s' +); // Observes object's width changes -object.observe('widthchange', observerFunction); +object.observe( + 'widthchange', observerFunction +); ``` A question arises: how frequently and at what time fractions the `observerFunction` will be called? Let's assume in the first SDK version we emulated step-by-step animation at 10 frames per second: then the `observerFunction` will be called 10 times, getting values '140px', '180px', etc., up to '500px'. But then in a new API version, we switched to implementing both functions atop of a system's native functionality — and so you simply don't know, when and how frequently the `observerFunction` will be called. diff --git a/src/en/clean-copy/03-Section II. The Backwards Compatibility/03.md b/src/en/clean-copy/03-Section II. The Backwards Compatibility/03.md index 1f19e3a..aa04f45 100644 --- a/src/en/clean-copy/03-Section II. The Backwards Compatibility/03.md +++ b/src/en/clean-copy/03-Section II. The Backwards Compatibility/03.md @@ -65,7 +65,8 @@ More specifically, if we talk about changing available order options, we should 2. Add new ‘with-options’ endpoint: ``` - PUT /v1/partners/{partner_id}/coffee-machines-with-options + PUT /v1/partners/{partner_id}⮠ + /coffee-machines-with-options { "coffee_machines": [{ "id", diff --git a/src/en/clean-copy/03-Section II. The Backwards Compatibility/04.md b/src/en/clean-copy/03-Section II. The Backwards Compatibility/04.md index b70a62d..9c2e887 100644 --- a/src/en/clean-copy/03-Section II. The Backwards Compatibility/04.md +++ b/src/en/clean-copy/03-Section II. The Backwards Compatibility/04.md @@ -179,8 +179,14 @@ POST /v1/recipe-builder // Add all the formatters needed "formatters": { "volume": [ - { "language_code", "template" }, - { "language_code", "country_code", "template" } + { + "language_code", + "template" + }, { + "language_code", + "country_code", + "template" + } ] }, // Other actions needed to be done @@ -203,7 +209,8 @@ POST /v1/recipes/custom } → { - "id": "my-coffee-company:lungo-customato" + "id": + "my-coffee-company:lungo-customato" } ``` diff --git a/src/en/clean-copy/03-Section II. The Backwards Compatibility/05.md b/src/en/clean-copy/03-Section II. The Backwards Compatibility/05.md index 5ffe602..9855db2 100644 --- a/src/en/clean-copy/03-Section II. The Backwards Compatibility/05.md +++ b/src/en/clean-copy/03-Section II. The Backwards Compatibility/05.md @@ -91,21 +91,28 @@ There are different techniques to organize this data flow, but, basically, we al ``` /* Partner's implementation of the program run procedure for a custom API type */ -registerProgramRunHandler(apiType, (program) => { - // Initiating an execution - // on partner's side - let execution = initExecution(…); - // Listen to parent context's changes - program.context.on('takeout_requested', () => { - // If takeout is requested, initiate - // corresponding procedures - execution.prepareTakeout(() => { - // When the cup is ready for takeout, - // emit corresponding event - // for higher-level entity to catch it - execution.context.emit('takeout_ready'); - }); - }); +registerProgramRunHandler( + apiType, + (program) => { + // Initiating an execution + // on partner's side + let execution = initExecution(…); + // Listen to parent context's changes + program.context.on( + 'takeout_requested', + () => { + // If takeout is requested, initiate + // corresponding procedures + execution.prepareTakeout(() => { + // When the cup is ready for takeout, + // emit corresponding event for + // a higher-level entity to catch it + execution.context + .emit('takeout_ready'); + } + ); + } + ); return execution.context; }); @@ -139,30 +146,38 @@ It becomes obvious from what was said above that two-way weak coupling means a s ``` /* Partner's implementation of program run procedure for a custom API type */ -registerProgramRunHandler(apiType, (program) => { - // Initiating an execution - // on partner's side - let execution = initExecution(…); - // Listen to parent context's changes - program.context.on('takeout_requested', () => { - // If takeout is requested, initiate - // corresponding procedures - execution.prepareTakeout(() => { - /* When the order is ready for takeout, - signalize about that, but not - with event emitting */ - // execution.context.emit('takeout_ready') - program.context.set('takeout_ready'); - // Or even more rigidly - // program.setTakeoutReady(); +registerProgramRunHandler( + apiType, + (program) => { + // Initiating an execution + // on partner's side + let execution = initExecution(…); + // Listen to parent context's changes + program.context.on( + 'takeout_requested', + () => { + // If takeout is requested, initiate + // corresponding procedures + execution.prepareTakeout(() => { + /* When the order is ready + for takeout, signalize about that + by calling a method, not + with event emitting */ + // execution.context + // .emit('takeout_ready') + program.context + .set('takeout_ready'); + // Or even more rigidly + // program.setTakeoutReady(); + } + ); }); - }); - /* Since we're modifying parent context - instead of emitting events, we don't - actually need to return anything */ - // return execution.context; -}); -} + /* Since we're modifying parent context + instead of emitting events, we don't + actually need to return anything */ + // return execution.context; + } +); ``` Again, this solution might look counter-intuitive, since we efficiently returned to strong coupling via strictly defined methods. But there is an important difference: we're making all this stuff up because we expect alternative implementations of the *lower* abstraction level. Situations with different realizations of *higher* abstraction levels emerging are, of course, possible, but quite rare. The tree of alternative implementations usually grows from top to bottom.