diff --git a/docs/API.en.epub b/docs/API.en.epub index ef26c3b..6dd48ee 100644 Binary files a/docs/API.en.epub and b/docs/API.en.epub differ diff --git a/docs/API.en.html b/docs/API.en.html index a16cf04..84ccd8f 100644 --- a/docs/API.en.html +++ b/docs/API.en.html @@ -616,10 +616,10 @@ ul.references li p a.back-anchor {

In other words, hundreds or even thousands of different APIs must work correctly to make basic actions possible such as viewing a webpage. Modern Internet technologies simply couldn't exist without these tons of APIs working fine.

An API is an obligation. A formal obligation to connect different programmable contexts.

When I'm asked for an example of a well-designed API, I usually show a picture of a Roman aqueduct:

-
The Pont-du-Gard aqueduct. Built in the 1st century AD.  Image Credit: igorelick @ pixabay
The Pont-du-Gard aqueduct. Built in the 1st century AD. Image Credit: igorelick @ pixabay
+
The Pont-du-Gard aqueduct. Built in the 1st century AD.  Image Credit: igorelick @ pixabay
The Pont-du-Gard aqueduct. Built in the 1st century AD. Image Credit: igorelick @ pixabay

What differs between a Roman aqueduct and a good API is that in the case of APIs, the contract is presumed to be programmable. To connect the two areas, writing some code is needed. The goal of this book is to help you design APIs that serve their purposes as solidly as a Roman aqueduct does.

An aqueduct also illustrates another problem with the API design: your customers are engineers themselves. You are not supplying water to end-users. Suppliers are plugging their pipes into your engineering structure, building their own structures upon it. On the one hand, you may provide access to water to many more people through them, not spending your time plugging each individual house into your network. On the other hand, you can't control the quality of suppliers' solutions, and you are to blame every time there is a water problem caused by their incompetence.

@@ -627,23 +627,23 @@ ul.references li p a.back-anchor {

In the first three sections of this book, we aim to discuss API design in general, not bound to any specific technology. The concepts we describe are equally applicable to, let's say, web services and operating system (OS) APIs.

Still, two main scenarios dominate the stage when we talk about API development:

In the first case, we almost universally talk about APIs working atop the HTTP protocol. Today, the only notable examples of non-HTTP-based client-server interaction protocols are WebSocket (though it might, and frequently does, work in conjunction with HTTP), MQTT, and highly specialized APIs like media streaming and broadcasting formats.

HTTP API

Although the technology looks homogeneous because of using the same application-level protocol, in reality, there is significant diversity regarding different approaches to realizing HTTP-based APIs.

First, implementations differ in terms of utilizing HTTP capabilities:

The APIs that belong to the first category are usually denoted as “REST” or “RESTful” APIs. The second category comprises different RPC formats and some service protocols, for example, SSH.

Second, different HTTP APIs rely on different data formats:

All the above-mentioned technologies operate in significantly dissimilar paradigms, which give rise to rather hot “holy war” debates among software engineers. However, at the moment this book is being written we observe the choice for general-purpose APIs is reduced to the “REST API (in fact, JSON-over-HTTP) vs. gRPC vs. GraphQL” triad.

SDKs

@@ -654,20 +654,20 @@ ul.references li p a.back-anchor {

Let's discuss the second question first. Obviously, API “finesse” is primarily defined through its capability to solve developers' and users' problems. (One could reasonably argue that solving problems might not be the main purpose of offering an API to developers. However, manipulating public opinion is not of interest to the author of this book. Here we assume that APIs exist primarily to help people, not for some other covertly declared purposes.)

So, how might a “fine” API design assist developers in solving their (and their users') problems? Quite simply: a well-designed API allows developers to do their jobs in the most efficient and convenient manner. The distance from formulating a task to writing working code must be as short as possible. Among other things, this means that:

@@ -679,19 +679,19 @@ ul.references li p a.back-anchor {

The former means that the first (and sometimes the only) step in developing a service is creating an API for it, and we will discuss it in “The API Product” section of this book.

If we talk about the API-first approach in a technical sense, we mean the following: the contract, i.e. the obligation to connect two programmable contexts, precedes the implementation and defines it. More specifically, two rules must be respected:

The “specification” in this context is a formal machine-readable description of the contract in one of the interface definition languages (IDL) — for example, in the form of a Swagger/OpenAPI document or a .proto file.

Both rules assert that partner developers' interests are given the highest priority:

Therefore, for your API consumers, the API-first approach is a guarantee of a kind. However, it only works if the API was initially well-designed. If some irreparable flaws in the specification surface, we would have no other option but to break rule #2.

Chapter 6. On Backward Compatibility 

Backward compatibility is a temporal characteristic of an API. The obligation to maintain backward compatibility is the crucial point where API development differs from software development in general.

@@ -738,14 +738,14 @@ Cache-Control: no-cache

It should be read like this:

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 with “client.”

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

@@ -754,10 +754,10 @@ Cache-Control: no-cache

Apart from HTTP API notation, we will employ C-style pseudocode, or, to be more precise, JavaScript-like or Python-like one since types are omitted. We assume such imperative structures are readable enough to skip detailed grammar explanations. HTTP API-like samples intend to illustrate the contract, i.e., how we would design an API. Samples in pseudocode are intended to illustrate how developers might work with the API in their code, or how we would implement SDKs based on the contract.

Section I. The API Design

Chapter 9. The API Contexts Pyramid 

The approach we use to design APIs comprises four steps:

This four-step algorithm actually builds an API from top to bottom, from common requirements and use case scenarios down to a refined nomenclature of entities. In fact, moving this way will eventually conclude with a ready-to-use API, and that's why we value this approach highly.

It might seem that the most useful pieces of advice are given in the last chapter, but that's not true. The cost of a mistake made at certain levels differs. Fixing the naming is simple; revising the wrong understanding of what the API stands for is practically impossible.

@@ -808,13 +808,13 @@ Cache-Control: no-cache

What and How

After finishing all these theoretical exercises, we should proceed directly to designing and developing the API, having a decent understanding of two things:

In our coffee case, we are:

Chapter 11. Separating Abstraction Levels 

“Separate abstraction levels in your code” is possibly the most general advice for software developers. However, we don't think it would be a grave exaggeration to say that separating abstraction levels is also the most challenging task for API developers.

Before proceeding to the theory, we should clearly formulate why abstraction levels are so important, and what goals we're trying to achieve by separating them.

@@ -855,8 +855,8 @@ GET /v1/orders/{id}

Let's consider a question: how exactly should developers determine whether the order is ready or not? Let's say we do the following:

GET /v1/recipes/lungo
 →
@@ -887,8 +887,8 @@ GET /v1/orders/{id}
 

For those orders with an arbitrary volume requested, a developer will need to obtain the requested volume, not from the GET /v1/recipes endpoint, but the GET /v1/orders one. Doing so we're getting a whole bunch of related problems:

So we will get this:

GET /v1/orders/{id}
@@ -921,8 +921,8 @@ GET /v1/orders/{id}
 

The more the distance between programmable contexts our API connects, the deeper the hierarchy of the entities we are to develop.

In our example with coffee readiness detection, we clearly face the situation when we need an interim abstraction level:

A naïve approach to this situation is to design an interim abstraction level as a “connecting link,” which reformulates tasks from one abstraction level into another. For example, introduce a task entity like that:

{
@@ -957,8 +957,8 @@ GET /v1/orders/{id}
 

An experienced developer in this case must ask: what options do exist? how should we really determine the readiness of the beverage? If it turns out that comparing volumes is the only working method to tell whether the beverage is ready, then all the speculations above are wrong. You may safely include readiness-by-volume detection into your interfaces since no other methods exist. Before abstracting something we need to learn what exactly we're abstracting.

In our example let's assume that we have studied coffee machines' API specs, and learned that two device types exist:

    -
  • coffee machines capable of executing programs coded in the firmware; the only customizable options are some beverage parameters, like the desired volume, a syrup flavor, and a kind of milk;
  • -
  • coffee machines with built-in functions, like “grind specified coffee volume,” “shed the specified amount of water,” etc.; such coffee machines lack “preparation programs,” but provide access to commands and sensors.
  • +
  • Coffee machines capable of executing programs coded in the firmware; the only customizable options are some beverage parameters, like the desired volume, a syrup flavor, and a kind of milk
  • +
  • Coffee machines with built-in functions, like “grind specified coffee volume,” “shed the specified amount of water,” etc.; such coffee machines lack “preparation programs,” but provide access to commands and sensors.

To be more specific, let's assume those two kinds of coffee machines provide the following physical API.

    @@ -1002,7 +1002,7 @@ POST /cancel // as in the `POST /execute` method GET /execution/{id}/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 will really get something like that from vendors, and this API is actually quite a sane one.

+

NB: 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 will really get something like that from vendors, and this API is actually quite a sane one.

  • Coffee machines with built-in functions:

    @@ -1074,8 +1074,8 @@ GET /sensors

    Note, that the first-kind API is much closer to developers' needs than the second-kind API. An indivisible “program” is a way more convenient concept than working with raw commands and sensor data. There are only two problems we see in the first-kind API:

      -
    • absence of explicit “programs” to “recipes” relation; program identifier is of no use to developers since there is a “recipe” concept;
    • -
    • absence of an explicit “ready” status.
    • +
    • Absence of explicit “programs” to “recipes” relation. A program identifier is of no use to developers since there is a “recipe” concept.
    • +
    • Absence of an explicit “ready” status.

    But with the second-kind API, it's much worse. The main problem we foresee is the absence of “memory” for actions being executed. The functions and sensors API is totally stateless, which means we don't even understand who called a function being currently executed, when, or to what order it relates.

    So we need to introduce two abstraction levels.

    @@ -1083,8 +1083,8 @@ GET /sensors
  • Execution control level, which provides a uniform interface to indivisible programs. “Uniform interface” means here that, regardless of a coffee machine's kind, developers may expect:

      -
    • statuses and other high-level execution parameters nomenclature (for example, estimated preparation time or possible execution errors) being the same;
    • -
    • methods nomenclature (for example, order cancellation method) and their behavior being the same.
    • +
    • Statuses and other high-level execution parameters nomenclature (for example, estimated preparation time or possible execution errors) being the same;
    • +
    • Methods nomenclature (for example, order cancellation method) and their behavior being the same.
  • @@ -1129,8 +1129,8 @@ GET /sensors

    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. The run method handler is capable of extracting all the program metadata and performing one of two actions:

      -
    • call the POST /execute physical API method, passing the internal program identifier for the first API kind;
    • -
    • initiate runtime creation to proceed with the second API kind.
    • +
    • Call the POST /execute physical API method, passing the internal program identifier for the first API kind
    • +
    • Initiate runtime creation to proceed with the second API kind.

    Out of general considerations, the 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” (i.e., a stateful execution context) to run a program and control its state.

    POST /v1/runtimes
    @@ -1190,8 +1190,8 @@ GET /sensors
     

    NB: when implementing the ordersmatchrunruntimes call sequence, we have two options:

      -
    • either POST /orders handler requests the data regarding the recipe, the coffee machine model, and the program on its own, and forms a stateless request that contains all necessary data (API kind, command sequence, etc.);
    • -
    • or the request contains only data identifiers, and the next handler in the chain will request pieces of data it needs via some internal APIs.
    • +
    • Either POST /orders handler requests the data regarding the recipe, the coffee machine model, and the program on its own, and forms a stateless request that contains all necessary data (API kind, command sequence, etc.)
    • +
    • Or the request contains only data identifiers, and the next handler in the chain will request pieces of data it needs via some internal APIs.

    Both variants are plausible and the selection between them depends on implementation details.

    Abstraction Levels Isolation

    @@ -1202,16 +1202,16 @@ GET /sensors
  • The orders handler completes operations on its level of responsibility (e.g., checks user authorization), finds the program_run_id identifier and performs a call to the runs/{program_run_id} endpoint.
  • The runs endpoint completes operations corresponding to its level (e.g., checks the coffee machine API kind) and, depending on the API kind, proceeds with one of two possible execution branches:
      -
    • either calls the GET /execution/status method of the physical coffee machine API, gets the coffee volume, and compares it to the reference value
    • -
    • or invokes the GET /v1/runtimes/{runtime_id} method to obtain the state.status and converts it to the order status.
    • +
    • Either calls the GET /execution/status method of the physical coffee machine API, gets the coffee volume, and compares it to the reference value or
    • +
    • Invokes the GET /v1/runtimes/{runtime_id} method to obtain the state.status and converts it to the order status.
  • In the case of the second-kind API, the call chain continues: the GET /runtimes handler invokes the GET /sensors method of the physical coffee machine API and performs some manipulations with the data, like comparing the cup / ground coffee / shed water volumes with the reference ones, and changing the state and the status if needed.
  • NB: The term “call chain” shouldn't be taken literally. Each abstraction level may be organized differently in a technical sense. For example:

    Note what happens here: each abstraction level wields its own status (i.e., order, runtime, and sensors status respectively) formulated in subject area terms corresponding to this level. Forbidding “jumping over” results in the necessity to spawn statuses at each level independently.

    Now let's examine how the order cancel operation flows through our abstraction levels. In this case, the call chain will look like this:

    @@ -1219,21 +1219,21 @@ GET /sensors
  • A user initiates a call to the POST /v1/orders/{id}/cancel method.
  • The method handler completes operations on its level of responsibility:
      -
    • checks the authorization
    • -
    • resolves money issues (e.g., whether a refund is needed)
    • -
    • finds the program_run_id identifier and calls the runs/{program_run_id}/cancel method.
    • +
    • Checks the authorization
    • +
    • Resolves money issues (e.g., whether a refund is needed)
    • +
    • Finds the program_run_id identifier and calls the runs/{program_run_id}/cancel method.
  • The runs/cancel handler completes operations on its level of responsibility and, depending on the coffee machine API kind, proceeds with one of two possible execution branches:
      -
    • calls the POST /execution/cancel method of a physical coffee machine API, or
    • -
    • invokes the POST /v1/runtimes/{id}/terminate method.
    • +
    • Calls the POST /execution/cancel method of a physical coffee machine API
    • +
    • Or invokes the POST /v1/runtimes/{id}/terminate method.
  • In the second case, the call chain continues as the terminate handler operates its internal state:
      -
    • changes the resolution to "terminated"
    • -
    • runs the "discard_cup" command.
    • +
    • Changes the resolution to "terminated"
    • +
    • Runs the "discard_cup" command.
  • @@ -1242,9 +1242,13 @@ GET /sensors
  • At each abstraction level the idea of “order canceling” is reformulated:

      -
    • at the orders level, this action splits into several “cancels” of other levels: you need to cancel money holding and cancel order execution
    • -
    • at the second API kind, physical level the “cancel” operation itself doesn't exist; “cancel” means “executing the discard_cup command,” which is quite the same as any other command. -The interim API level is needed to make this transition between different level “cancels” smooth and rational without jumping over canyons.
    • +
    • +

      At the orders level, this action splits into several “cancels” of other levels: you need to cancel money holding and cancel order execution

      +
    • +
    • +

      At the second API kind, physical level the “cancel” operation itself doesn't exist; “cancel” means “executing the discard_cup command,” which is quite the same as any other command.

      +

      The interim API level is needed to make this transition between different level “cancels” smooth and rational without jumping over canyons.

      +
  • @@ -1284,15 +1288,15 @@ It is important to note that we don't calculate new variables out of sensor data

    Also, if we take a deeper look at the “bad” decision (forcing developers to determine the actual order status on their own), being discussed at the beginning of this chapter, we could notice a data flow collision there:

      -
    • on the one hand, in the order context “leaked” physical data (beverage volume prepared) is injected, stirring abstraction levels irreversibly
    • -
    • on the other hand, the order context itself is deficient: it doesn't provide new meta-variables non-existent at the lower levels (the order status, in particular), doesn't initialize them, and doesn't set the game rules.
    • +
    • On one hand, in the order context “leaked” physical data (beverage volume prepared) is injected, stirring abstraction levels irreversibly
    • +
    • On the other hand, the order context itself is deficient: it doesn't provide new meta-variables non-existent at the lower levels (the order status, in particular), doesn't initialize them, and doesn't set the game rules.

    We will discuss data contexts in more detail in Section II. Here we will just state that data flows and their transformations might be and must be examined as a specific API facet, which helps us separate abstraction levels properly and check if our theoretical concepts work as intended.

    Chapter 12. Isolating Responsibility Areas 

    In the previous chapter, we concluded that the hierarchy of abstractions in our hypothetical project would comprise:

      -
    • the user level (the entities formulated in terms understandable by users and acted upon by them: orders, coffee recipes);
    • -
    • the program execution control level (the entities responsible for transforming orders into machine commands);
    • -
    • the runtime level for the second API kind (the entities describing the command execution state machine).
    • +
    • The user level (the entities formulated in terms understandable by users and acted upon by them: orders, coffee recipes)
    • +
    • The program execution control level (the entities responsible for transforming orders into machine commands)
    • +
    • The runtime level for the second API kind (the entities describing the command execution state machine).

    We are now to define each entity's responsibility area: what's the reasoning for keeping this entity within our API boundaries? What operations are applicable to the entity directly (and which are delegated to other objects)? In fact, we are to apply the “why”-principle to every single API entity.

    To do so, we must iterate all over the API and formulate in subject area terms what every object is. Let us remind that the abstraction levels concept implies that each level is some interim subject area per se; a step we take in the journey from describing a task in terms belonging to the first connected context (“a lungo ordered by a user”) to terms belonging to the second connected context (“a command performed by a coffee machine”).

    @@ -1302,10 +1306,10 @@ It is important to note that we don't calculate new variables out of sensor data
    • An order describes some logical unit in app-user interaction. An order might be:
        -
      • created
      • -
      • checked for its status
      • -
      • retrieved
      • -
      • canceled.
      • +
      • Created
      • +
      • Checked for its status
      • +
      • Retrieved
      • +
      • Canceled.
    • A recipe describes an “ideal model” of a coffee beverage type, i.e., its customer properties. A recipe is an immutable entity that can only be read.
    • @@ -1318,9 +1322,9 @@ It is important to note that we don't calculate new variables out of sensor data
    • The programs/matcher entity couples a recipe and a program, which in fact means retrieving a dataset needed to prepare a specific recipe on a specific coffee machine.
    • The programs/run entity describes a single fact of running a program on a coffee machine. A run might be:
        -
      • initialized (created)
      • -
      • checked for its status
      • -
      • canceled.
      • +
      • Initialized (created)
      • +
      • Checked for its status
      • +
      • Canceled.
    @@ -1329,9 +1333,9 @@ It is important to note that we don't calculate new variables out of sensor data
    • A runtime describes a specific execution data context, i.e., the state of each variable. A runtime can be:
        -
      • initialized (created)
      • -
      • checked for its status
      • -
      • terminated.
      • +
      • Initialized (created)
      • +
      • Checked for its status
      • +
      • Terminated.
    @@ -1343,9 +1347,9 @@ It is important to note that we don't calculate new variables out of sensor data

    So, let us imagine we've got a task to write an app for ordering coffee based on our API. What code would we write?

    Obviously, the first step is to offer a choice to the user, to make them point out what they want. And this very first step reveals that our API is quite inconvenient. There are no methods allowing for choosing something. Developers have to implement these steps:

      -
    • retrieve all possible recipes from the GET /v1/recipes endpoint;
    • -
    • retrieve a list of all available coffee machines from the GET /v1/coffee-machines endpoint;
    • -
    • write code that traverses all this data.
    • +
    • Retrieve all possible recipes from the GET /v1/recipes endpoint
    • +
    • Retrieve a list of all available coffee machines from the GET /v1/coffee-machines endpoint
    • +
    • Write code that traverses all this data.

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

    // Retrieve all possible recipes
    @@ -1374,8 +1378,8 @@ app.display(matchingCoffeeMachines);
     

    As you see, developers are to write a lot of redundant code (to say nothing about the complexity of implementing spatial indexes). Besides, if we take into consideration our Napoleonic plans to cover all coffee machines in the world with our API, then we need to admit that this algorithm is just a waste of computational resources on retrieving lists and indexing them.

    The necessity of adding a new endpoint for searching becomes obvious. To design such an interface we must imagine ourselves being UX designers, and think about how an app could try to arouse users' interest. Two scenarios are evident:

      -
    • display all cafes in the vicinity and the types of coffee they offer (a “service discovery” scenario) — for new users or just users with no specific preferences;
    • -
    • display nearby cafes where a user could order a particular type of coffee — for users seeking a certain beverage type.
    • +
    • Display all cafes in the vicinity and the types of coffee they offer (a “service discovery” scenario) — for new users or just users with no specific preferences
    • +
    • Display nearby cafes where a user could order a particular type of coffee — for users seeking a certain beverage type.

    Then our new interface would look like this:

    POST /v1/offers/search
    @@ -1419,9 +1423,9 @@ app.display(offers);
     

    Methods similar to the newly invented offers/search one are called helpers. The purpose of their existence is to generalize known API usage scenarios and facilitate their implementation. By “facilitating,” we mean not only reducing wordiness (getting rid of “boilerplates”) but also helping developers avoid common problems and mistakes.

    For instance, let's consider the problem of the monetary value of an order. Our search function returns some “offers” with prices. However, the price is volatile; coffee could cost less during “happy hours,” for example. Developers could make a mistake three times while implementing this functionality:

      -
    • cache search results on a client device for too long (as a result, the price will always be outdated);
    • -
    • contrary to the previous point, call the search endpoint excessively just to actualize prices, thus overloading the network and the API servers;
    • -
    • create an order with an invalid price (thereby deceiving a user, displaying one sum, and debiting another).
    • +
    • Cache search results on a client device for too long (as a result, the price will always be outdated).
    • +
    • Contrary to the previous point, call the search endpoint excessively just to actualize prices, thus overloading the network and the API servers.
    • +
    • Create an order with an invalid price (thereby deceiving a user, displaying one sum, and debiting another).

    To solve the third problem we could demand that the displayed price be included in the order creation request and return an error if it differs from the actual one. (In fact, any API working with money must do so.) However, this solution does not help with the first two problems, and also deteriorates the user experience. Displaying the actual price is always a much more convenient behavior than displaying errors upon pressing the “place an order” button.

    One solution is to provide a special identifier to an offer. This identifier must be specified in an order creation request:

    @@ -1529,13 +1533,13 @@ For example, the invalid price error is resolvable: a client could obtain a new

    This approach is regretfully quite common and could be found in almost every API. Fields are mixed into one single list and often prefixed to indicate the related ones.

    In this situation, we need to split this structure into data domains by grouping fields that are logically related to a single subject area. In our case, we may identify at least 7 data clusters:

      -
    • data regarding the place where the coffee machine is located
    • -
    • properties of the coffee machine itself
    • -
    • route data
    • -
    • recipe data
    • -
    • order options
    • -
    • offer data
    • -
    • pricing data.
    • +
    • Data regarding the place where the coffee machine is located
    • +
    • Properties of the coffee machine itself
    • +
    • Route data
    • +
    • Recipe data
    • +
    • Order options
    • +
    • Offer data
    • +
    • Pricing data.

    Let's group them together:

    {
    @@ -1692,9 +1696,9 @@ str_replace(needle, replace, haystack)
     

    Several rules are violated:

      -
    • the usage of an underscore is not consistent
    • -
    • functionally close methods have different needle/haystack argument ordering
    • -
    • the first function finds the first occurrence while the second one finds all occurrences, and there is no way to deduce that fact from the function signatures.
    • +
    • The usage of an underscore is not consistent
    • +
    • Functionally close methods have different needle/haystack argument ordering
    • +
    • The first function finds the first occurrence while the second one finds all occurrences, and there is no way to deduce that fact from the function signatures.

    Improving these function signatures is left as an exercise for the reader.

    8. Avoid Double Negations
    @@ -1797,9 +1801,9 @@ PUT /v1/users/{id}

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

    There are three obvious reasons for inflating network traffic:

      -
    • clients query the data too frequently or cache it too little
    • -
    • no data pagination is provided
    • -
    • no limits are set on the data fields, or too large binary data (graphics, audio, video, etc.) is transmitted.
    • +
    • Clients query the data too frequently or cache it too little
    • +
    • No data pagination is provided
    • +
    • No limits are set on the data fields, or too large binary data (graphics, audio, video, etc.) is transmitted.

    All these problems must be addressed by setting limitations on field sizes and properly decomposing endpoints. If an entity comprises both “lightweight” data (such as the name and description of a recipe) and “heavy” data (such as the promotional picture of a 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 (e.g., a link to the image). This will also allow for 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 (e.g., making a single order) to count the number of requests and the amount of traffic it requires. It might turn out that the high number of requests or increased network traffic consumption is due to a mistake in the design of state change notification endpoints. We will discuss this issue in detail in the “Bidirectional Data Flow” chapter of “The API Patterns” section of this book.

    @@ -2534,9 +2538,9 @@ POST /v1/runtimes/{id}/terminate

    Let's proceed to the technical problems that API developers face. We begin with the last one described in the introductory chapter: the necessity to synchronize states. Let us imagine that a user creates a request to order coffee through our API. While this request travels from the client to the coffee house and back, many things might happen. Consider the following chain of events:

    1. The client sends the order creation request
    2. -
    3. Because of network issues, the request propagates to the server very slowly, and the client gets a timeout; +
    4. Because of network issues, the request propagates to the server very slowly, and the client gets a timeout
        -
      • therefore, the client does not know whether the query was served or not.
      • +
      • Therefore, the client does not know whether the query was served or not.
    5. The client requests the current state of the system and gets an empty response as the initial request still hasn't reached the server: @@ -2653,24 +2657,24 @@ const pendingOrders = await api.

    Such a token might be:

      -
    • an identifier (or identifiers) of the last modifying operations carried out by the client;
    • -
    • the last known resource version (modification date, ETag) known to the client.
    • +
    • An identifier (or identifiers) of the last modifying operations carried out by the client
    • +
    • The last known resource version (modification date, ETag) known to the client.

    Upon getting the token, the server must check that the response (e.g., the list of ongoing operations it returns) matches the token, i.e., the eventual consistency converged. If it did not (the client passed the modification date / version / last order id newer than the one known to the server), one of the following policies or their combinations might be applied:

      -
    • the server might repeat the request to the underlying DB or to the other kind of data storage in order to get the newest version (eventually);
    • -
    • the server might return an error that requires the client to try again later;
    • -
    • the server queries the main node of the DB, if such a thing exists, or otherwise initiates retrieving the master data.
    • +
    • The server might repeat the request to the underlying DB or to the other kind of data storage in order to get the newest version (eventually)
    • +
    • The server might return an error that requires the client to try again later
    • +
    • The server queries the main node of the DB, if such a thing exists, or otherwise initiates retrieving the master data.

    The advantage of this approach is client development convenience (compared to the absence of any guarantees): by preserving the version token, client developers get rid of the possible inconsistency of the data got from API endpoints. There are two disadvantages, however:

      -
    • it is still a trade-off between system scalability and a constant inflow of background errors; +
    • It is still a trade-off between system scalability and a constant inflow of background errors:
        -
      • if you're querying master data or repeating the request upon the version mismatch, the load on the master storage is increased in poorly a predictable manner;
      • -
      • if you return a client error instead, the number of such errors might be considerable, and partners will need to write some additional code to deal with the errors;
      • +
      • If you're querying master data or repeating the request upon the version mismatch, the load on the master storage is increased in poorly a predictable manner
      • +
      • If you return a client error instead, the number of such errors might be considerable, and partners will need to write some additional code to deal with the errors.
    • -
    • this approach is still probabilistic, and will only help in a limited number of use cases (to be discussed below).
    • +
    • This approach is still probabilistic, and will only help in a limited number of use cases (to be discussed below).

    There is also an important question regarding the default behavior of the server if no version token was passed. Theoretically, in this case, master data should be returned, as the absence of the token might be the result of an app crash and subsequent restart or corrupted data storage. However, this implies an additional load on the master node.

    Evaluating the Risks of Switching to Eventual Consistency

    @@ -2678,19 +2682,19 @@ const pendingOrders = await api.

    NB: the “typical usage profile” stipulation is important: an API implies the variability of client scenarios, and API usage cases might fall into several groups, each featuring quite different error profiles. The classical example is client APIs (where it's an end user who makes actions and waits for results) versus server APIs (where the execution time is per se not so important — but let's say mass parallel execution might be). If this happens, it's a strong signal to make a family of API products covering different usage scenarios, as we will discuss in “The API Services Range” chapter of “The API Product” section of this book.

    Let's return to the coffee example, and imagine we implemented the following scheme:

      -
    • optimistic concurrency control (through, let's say, the id of the last user's order)
    • -
    • the “read-your-writes” policy of reading the order list (again with passing the last known order id as a token)
    • -
    • retrieving master data in the case the token is absent.
    • +
    • Optimistic concurrency control (through, let's say, the id of the last user's order)
    • +
    • The “read-your-writes” policy of reading the order list (again with passing the last known order id as a token)
    • +
    • Retrieving master data in the case the token is absent.

    In this case, the order creation error might only happen in one of the two cases:

      -
    • the client works with the data incorrectly (does not preserve the identifier of the last order or the idempotency key while repeating the request)
    • -
    • the client tries to create an order from two different instances of the app that do not share the common state.
    • +
    • The client works with the data incorrectly (does not preserve the identifier of the last order or the idempotency key while repeating the request)
    • +
    • The client tries to create an order from two different instances of the app that do not share the common state.

    The first case means there is a bug in the partner's code; the second case means that the user is deliberately testing the system's stability — which is hardly a frequent case (or, let's say, the user's phone went off and they quickly switched to a tablet — rather rare case as well, we must admit).

    Let's now imagine that we dropped the third requirement — i.e., returning the master data if the token was not provided by the client. We would get the third case when the client gets an error:

      -
    • the client application lost some data (restarted or corrupted), and the user tries to replicate the last request.
    • +
    • The client application lost some data (restarted or corrupted), and the user tries to replicate the last request.

    NB: the repeated request might happen without any automation involved if, let's say, the user got bored of waiting, killed the app and manually re-orders the coffee again.

    Mathematically, the probability of getting the error is expressed quite simply. It's the ratio between two durations: the time period needed to get the actual state to the time period needed to restart the app and repeat the request. (Keep in mind that the last failed request might be automatically repeated on startup by the client.) The former depends on the technical properties of the system (for instance, on the replication latency, i.e., the lag between the master and its read-only copies) while the latter depends on what client is repeating the call.

    @@ -2739,10 +2743,10 @@ const pendingOrders = await api.

    Thus we naturally came to the pattern of organizing asynchronous APIs through task queues. Here we use the term “asynchronous” logically meaning the absence of mutual logical locks: the party that makes a request gets a response immediately and does not wait until the requested procedure is fully carried out being able to continue to interact with the API. Technically in modern application environments, locking (of both the client and server) almost universally doesn't happen during long-responding calls. However, logically allowing users to work with the API while waiting for a response from a modifying endpoint is error-prone and leads to collisions like the one we described above.

    The asynchronous call pattern is useful for solving other practical tasks as well:

      -
    • caching operation results and providing links to them (implying that if the client needs to reread the operation result or share it with another client, it might use the task identifier to do so)
    • -
    • ensuring operation idempotency (through introducing the task confirmation step we will actually get the draft-commit system as discussed in the “Describing Final Interfaces” chapter)
    • -
    • naturally improving resilience to peak loads on the service as the new tasks will be queuing up (possibly prioritized) in fact implementing the “token bucket” technique
    • -
    • organizing interaction in the cases of very long-lasting operations that require more time than typical timeouts (which are tens of seconds in the case of network calls) or can take unpredictable time.
    • +
    • Caching operation results and providing links to them (implying that if the client needs to reread the operation result or share it with another client, it might use the task identifier to do so)
    • +
    • Ensuring operation idempotency (through introducing the task confirmation step we will actually get the draft-commit system as discussed in the “Describing Final Interfaces” chapter)
    • +
    • Naturally improving resilience to peak loads on the service as the new tasks will be queuing up (possibly prioritized) in fact implementing the “token bucket” technique
    • +
    • Organizing interaction in the cases of very long-lasting operations that require more time than typical timeouts (which are tens of seconds in the case of network calls) or can take unpredictable time.

    Also, asynchronous communication is more robust from a future API development point of view: request handling procedures might evolve towards prolonging and extending the asynchronous execution pipelines whereas synchronous handlers must retain reasonable execution times which puts certain restrictions on possible internal architecture.

    NB: in some APIs, an ambivalent decision is implemented where endpoints feature a double interface that might either return a result or a link to a task. Although from the API developer's point of view, this might look logical (if the request was processed “quickly”, e.g., served from cache, the result is to be returned immediately; otherwise, the asynchronous task is created), for API consumers, this solution is quite inconvenient as it forces them to maintain two execution branches in their code. Sometimes, a concept of providing a double set of endpoints (synchronous and asynchronous ones) is implemented, but this simply shifts the burden of making decisions onto partners.

    @@ -2758,9 +2762,9 @@ const pendingOrders = await api.
  • Employing task queues might lead to some problems specific to the queue technology itself, i.e., not related to the business logic of the request handler:

      -
    • tasks might be “lost” and never processed
    • -
    • events might be received in the wrong order or processed twice, which might affect public interfaces
    • -
    • under the task identifier, wrong data might be published (corresponding to some other task) or the data might be corrupted.
    • +
    • Tasks might be “lost” and never processed
    • +
    • Events might be received in the wrong order or processed twice, which might affect public interfaces
    • +
    • Under the task identifier, wrong data might be published (corresponding to some other task) or the data might be corrupted.

    These issues will be totally unexpected by developers and will lead to bugs in applications that are very hard to reproduce.

  • @@ -3064,7 +3068,7 @@ POST /v1/partners/{id}/offers/history⮠
  • The important case for such modifications is marking the received data as “read”.
  • -
  • finally, if the endpoint is needed to access only real-time “raw” data while the processed and classified data are available through other interfaces.
  • +
  • Finally, if the endpoint is needed to access only real-time “raw” data while the processed and classified data are available through other interfaces.
  • If none of the approaches above works, our only solution is changing the subject area itself. If we can't consistently enumerate list elements, we need to find a facet of the same data that we can enumerate. In our example with the ongoing orders we might make an ordered list of the events of creating new orders:

    // Retrieve all the events older
    @@ -3169,18 +3173,18 @@ GET /v1/orders/created-history⮠
     
  • The system state must be restorable. If the partner erroneously responded that messages are processed while they are not, there must be a possibility for them to redeem themselves and get the list of missed events and/or the full system state and fix all the issues
  • Help partners to write proper code by describing in the documentation all unobvious subtleties that inexperienced developers might be unaware of:
      -
    • idempotency keys for every operation
    • -
    • delivery guarantees (“at least once,” “exactly ones,” etc.; see the reference description on the example of Apache Kafka API)
    • -
    • possibility of the server generating parallel requests and the maximum number of such requests at a time
    • -
    • guarantees of message ordering (i.e., the notifications are always delivered ordered from the oldest one to the newest one) or the absence of such guarantees
    • -
    • the sizes of all messages and message fields in bytes
    • -
    • the retry policy in case an error is returned by the partner's server
    • +
    • Idempotency keys for every operation
    • +
    • Delivery guarantees (“at least once,” “exactly ones,” etc.; see the reference description on the example of Apache Kafka API)
    • +
    • Possibility of the server generating parallel requests and the maximum number of such requests at a time
    • +
    • Guarantees of message ordering (i.e., the notifications are always delivered ordered from the oldest one to the newest one) or the absence of such guarantees
    • +
    • The sizes of all messages and message fields in bytes
    • +
    • The retry policy in case an error is returned by the partner's server
  • Implement a monitoring system to check the health of partners' endpoints:
      -
    • if a large number of errors or timeouts occurs, it must be escalated (including notifying the partner about the problem), probably with several escalation tiers
    • -
    • if too many un-processed notifications are stuck, there must be a mechanism of controllable degradation (limiting the number of requests toward the partner, e.g. cutting the demand by disallowing some users to make an order) up to fully disconnecting the partner from the platform.
    • +
    • If a large number of errors or timeouts occurs, it must be escalated (including notifying the partner about the problem), probably with several escalation tiers,
    • +
    • If too many un-processed notifications are stuck, there must be a mechanism of controllable degradation (limiting the number of requests toward the partner, e.g. cutting the demand by disallowing some users to make an order) up to fully disconnecting the partner from the platform.
  • @@ -3189,9 +3193,9 @@ GET /v1/orders/created-history⮠

    To solve these problems, and also to ensure better horizontal scalability, message queues were developed, most notably numerous pub/sub pattern implementations. At present moment, pub/sub-based architectures are very popular in enterprise software development, up to switching any inter-service communication to message queues.

    NB: let us note that everything comes with a price, and these delivery guarantees and horizontal scalability are not an exclusion:

      -
    • all communication becomes eventually consistent with all the implications
    • -
    • decent horizontal scalability and cheap message queue usage are only achievable with at least once/at most once policies and no ordering guarantee
    • -
    • queues might accumulate unprocessed events, introducing increasing delays, and solving this issue on the subscriber's side might be quite non-trivial.
    • +
    • All communication becomes eventually consistent with all the implications
    • +
    • Decent horizontal scalability and cheap message queue usage are only achievable with at least once/at most once policies and no ordering guarantee
    • +
    • Queues might accumulate unprocessed events, introducing increasing delays, and solving this issue on the subscriber's side might be quite non-trivial.

    Also, in public APIs both technologies are frequently used in conjunction: the API backend sends a task to call the webhook in the form of publishing an event which the specially designed internal service will try to process by making the call.

    Theoretically, we can imagine an integration that exposes directly accessible message queues in one of the standard formats for partners to subscribe. However, we are unaware of any examples of such APIs.

    Chapter 22. Multiplexing Notifications. Asynchronous Event Processing 

    @@ -3579,11 +3583,11 @@ PATCH /v1/orders/{id}

    A full example of an API implementing the naïve approach would look like this:

    // Partially rewrites the order:
    -//   * resets the delivery address
    +//   * Resets the delivery address
     //     to the default values
    -//   * leaves the first beverage
    +//   * Leaves the first beverage
     //     intact
    -//   * removes the second beverage
    +//   * Removes the second beverage.
     PATCH /v1/orders/{id}
     {
       // “Special” value #1:
    @@ -3622,11 +3626,11 @@ PATCH /v1/orders/{id}
     
     

    The solution could be enhanced by introducing explicit control sequences instead of relying on “magical” values and adding meta settings for the operation (such as a field name filter as it's implemented in gRPC). Here's an example:

    // Partially rewrites the order:
    -//   * resets the delivery address
    +//   * Resets the delivery address
     //     to the default values
    -//   * leaves the first beverage
    +//   * Leaves the first beverage
     //     intact
    -//   * removes the second beverage
    +//   * Removes the second beverage.
     PATCH /v1/orders/{id}?⮠
       // A meta filter: which fields
       // are allowed to be modified
    @@ -3714,8 +3718,8 @@ DELETE /v1/orders/{id}/items/{item_id}
     

    The idea of applying changes to a resource state through independent atomic idempotent operations looks attractive as a conflict resolution technique as well. As subcomponents of the resource are fully overwritten, it is guaranteed that the result of applying the changes will be exactly what the user saw on the screen of their device, even if they had observed an outdated version of the resource. However, this approach helps very little if we need a high granularity of data editing as it's implemented in modern services for collaborative document editing and version control systems (as we will need to implement endpoints with the same level of granularity, literally one for each symbol in the document).

    To make true collaborative editing possible, a specifically designed format for describing changes needs to be implemented. It must allow for:

      -
    • ensuring the maximum granularity (each operation corresponds to one distinct user's action)
    • -
    • implementing conflict resolution policies.
    • +
    • Ensuring the maximum granularity (each operation corresponds to one distinct user's action)
    • +
    • Implementing conflict resolution policies.

    In our case, we might take this direction:

    POST /v1/order/changes
    @@ -3851,26 +3855,22 @@ X-Idempotency-Token: <token>
     
     

    Of course, preserving minor versions indefinitely is not possible (partly because of security and compliance issues that tend to accumulate). However, providing such access for a reasonable period of time is considered a hygienic norm for popular APIs.

    NB. Sometimes to defend the concept of a single accessible API version, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backward compatibility as it might rely on some unversioned services (for example, data in the DB shared between all API versions). However, we consider this an additional reason to isolate such dependencies (see “The Serenity Notepad” chapter) as it means that changes to these subsystems might result in the API becoming inoperable.

    Chapter 27. On the Waterline of the Iceberg 

    -

    Before we start talking about 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.

    +

    Before we start talking about extensible API design, we should discuss the hygienic minimum. Many problems would have never occurred if API vendors had paid more attention to clearly 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 (i.e., 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.

    +

    At any given moment, your API is like an iceberg: it comprises an observable (i.e., documented) part and a hidden undocumented one. If the API is properly designed, 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.

      -
    • -

      Computers exist to make complicated things easy, not vice versa. The code developers write upon your API must describe a complicated problem's solution in neat and straightforward sentences. If developers have to write more code than the API itself comprises, then there is something rotten here. Probably, this API simply isn't needed at all.

      -
    • -
    • -

      Revoking the API functionality causes losses. If you've promised to provide some functionality, you will have to do so “forever” (until this API version's maintenance period is over). Pronouncing some functionality deprecated is a tricky thing, potentially alienating your customers.

      -
    • +
    • Computers exist to make complicated things easy, not the other way around. The code that developers write using your API should describe a complicated problem's solution in neat and straightforward sentences. If developers have to write more code than the API itself comprises, then there is something rotten here. It's possible that this API isn't needed at all.
    • +
    • Revoking API functionality causes losses. If you have promised to provide certain functionality, you will have to do so “forever” (or at least until the maintenance period for that API version is over). Pronouncing some functionality as deprecated can be tricky and may alienate your customers.
    -

    A rule of thumb is very simple: if some functionality might be withheld — then never expose it until you really need to. It might be reformulated like this: every entity, every field, and every public API method is a product decision. There must be solid product reasons why some functionality is exposed.

    +

    The rule of thumb is very simple: if some functionality might be withheld, then never expose it until you really need to. It might be reformulated as follows: every entity, every field, and every public API method is a product decision. There must be solid product reasons why certain functionality is exposed.

    2. Avoid Gray Zones and Ambiguities
    -

    Your obligations to maintain some functionality must be stated as clearly as possible, especially regarding those environments and platforms where no native capability to restrict access to undocumented functionality exists. Unfortunately, developers tend to consider some private features they found to be eligible for use, thus presuming the API vendor shall maintain them intact. The policy on such “findings” must be articulated explicitly. At the very least, in the case of such non-authorized usage of undocumented functionality, you might refer to the docs and be within your rights in the eyes of the community.

    -

    However, API developers often legitimize such gray zones themselves, for example, by:

    +

    Your obligations to maintain some functionality must be stated as clearly as possible, especially when provided in environments and platforms where there is no native capability to restrict access to undocumented functionality. Unfortunately, developers often consider some private features they “discover” as eligible for use, assuming the API vendor shall maintain them intact. The policy regarding such “findings” must be explicitly articulated. At the very least, in the case of unauthorized usage of undocumented functionality, you can refer to the documentation and be within your rights in the eyes of the community.

    +

    However, API developers often legitimize these gray zones themselves. For example, by:

      -
    • returning undocumented fields in endpoint responses;
    • -
    • using private functionality in code samples — in the docs, while responding to support messages, in conference talks, etc.
    • +
    • Returning undocumented fields in endpoint responses
    • +
    • Using private functionality in code samples: in the documentation, responses to support inquiries, conference talks, etc.
    -

    One cannot make a partial commitment. Either you guarantee this code will always work or do not slip the slightest note such functionality exists.

    +

    One cannot make a partial commitment. Either you guarantee that the code will always work or do not slip the slightest note that such functionality exists.

    3. Codify Implicit Agreements

    The third principle is much less obvious. Pay close attention to the code that you're suggesting developers write: are there any conventions that you consider self-evident but never wrote down?

    Example #1. Let's take a look at this order processing SDK example:

    @@ -3884,17 +3884,17 @@ let status = api.getStatus(order.id);
    let order = api.createOrder();
     let status;
     while (true) {
    -    try {
    -        status = api.getStatus(order.id);
    -    } catch (e) {
    -        if (e.httpStatusCode != 404 ||
    -            timeoutExceeded()) {
    -            break;
    -        }
    +  try {
    +    status = api.getStatus(order.id);
    +  } catch (e) {
    +    if (e.httpStatusCode != 404 ||
    +      timeoutExceeded()) {
    +      break;
         }
    +  }
     }
     if (status) {
    -    …
    +  …
     }
     

    We presume we may skip the explanations of why such code must never be written under any circumstances. If you're really providing a non-strictly consistent API, then either the createOrder operation must be asynchronous and return the result when all replicas are synchronized, or the retry policy must be hidden inside the getStatus operation implementation.

    @@ -3950,72 +3950,72 @@ object.observe( ]}

    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 even "ready" without a "payment_approved" event being emitted. It might appear to you that this modification is backward-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 procedures, for example, gathering income and expenses analytics. It's quite logical to expect this code operates a state machine that switches from one state to another depending on 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 will have to cope with the error's cause. In the worst case, partners will operate the incorrect statistics for an indefinite period of time until they find the issue.

    +

    Let's assume that a developer (probably your company's business partner) wrote some code implementing valuable business procedures, for example, gathering income and expenses analytics. It's quite logical to expect this code operates a state machine that switches from one state to another depending on 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 will have to cope with the error's cause. In the worst case, partners will operate incorrect statistics for an indefinite period of time until they find the issue.

    A proper decision would be, first, documenting the event order and the allowed states; second, continuing to generate the "payment_approved" event before the "preparing_started" one (since you're making a decision to prepare that order, so you're in fact approving the payment) and add extended payment information.

    This example leads us to the last rule.

    4. Product Logic Must Be Backward-Compatible as Well
    -

    State transition graph, event order, possible causes of status changes, etc. — such critical things must be documented. However, not every piece of business logic can be defined in the form of a programmable contract; some cannot be represented in a machine-readable form at all.

    +

    The state transition graph, event order, possible causes of status changes, etc. — such critical things must be documented. However, not every piece of business logic can be defined in the form of a programmable contract; some cannot be represented in a machine-readable form at all.

    Imagine that one day you start taking phone calls. A client may contact the call center to cancel an order. You might even make this functionality technically backward-compatible by introducing new fields to the “order” entity. But the end-user might simply know the number and call it even if the app wasn't suggesting anything like that. The partner's business analytical code might be broken as well or start displaying weather on Mars since it was written without knowing about the possibility of canceling orders in circumvention of the partner's systems.

    A technically correct decision would be to add a “canceling via call center allowed” parameter to the order creation function. Conversely, call center operators might only cancel those orders that were created with this flag set. But that would be a bad decision from a product point of view because it is not obvious to users that they can cancel some orders by phone and not others. The only “good” decision in this situation is to foresee the possibility of external order cancellations in the first place. If you haven't foreseen it, your only option is the “Serenity Notepad” that will be discussed in the last chapter of this Section.

    Chapter 28. Extending through Abstracting 

    In the previous chapters, we have attempted to outline theoretical rules and illustrate them with practical examples. However, understanding the principles of designing change-proof APIs requires practice above all else. The ability to anticipate future growth problems comes from a handful of grave mistakes once made. While it is impossible to foresee everything, one can develop a certain technical intuition.

    -

    Therefore, in the following chapters, we will test the robustness our study API from the previous Section, examining it from various perspectives to perform a “variational analysis” of our interfaces. More specifically, we will apply a “What If?” question to every entity, as if we are to provide a possibility to write an alternate implementation of every piece of logic.

    -

    NB. In our examples, the interfaces will be constructed in a manner allowing for dynamic real-time linking of different entities. In practice, such integrations usually imply writing an ad hoc server-side code in accordance with specific agreements made with specific partners. But for educational purposes, we will pursue more abstract and complicated ways. Dynamic real-time linking is more typical in complex program constructs like operating system APIs or embeddable libraries; giving educational examples based on such sophisticated systems would be too inconvenient.

    -

    Let's start with the basics. Imagine that we haven't exposed any other functionality but searching for offers and making orders, thus providing an API of two methods: POST /offers/search and POST /orders.

    -

    Let us make the next logical step there and suppose that partners will wish to dynamically plug their own coffee machines (operating some previously unknown types of API) into our platform. To allow doing so, we have to negotiate a callback format that would allow us to call partners' APIs and expose two new endpoints providing the following capabilities:

    +

    Therefore, in the following chapters, we will test the robustness of our study API from the previous Section, examining it from various perspectives to perform a “variational analysis” of our interfaces. More specifically, we will apply a “What If?” question to every entity, as if we are to provide a possibility to write an alternate implementation of every piece of logic.

    +

    NB. In our examples, the interfaces will be constructed in a manner allowing for dynamic real-time linking of different entities. In practice, such integrations usually imply writing ad hoc server-side code in accordance with specific agreements made with specific partners. But for educational purposes, we will pursue more abstract and complicated ways. Dynamic real-time linking is more typical in complex program constructs like operating system APIs or embeddable libraries; giving educational examples based on such sophisticated systems would be too inconvenient.

    +

    Let's start with the basics. Imagine that we haven't exposed any other functionality but searching for offers and making orders, thus providing an API with two methods: POST /offers/search and POST /orders.

    +

    Let us take the next logical step and suppose that partners will wish to dynamically plug their own coffee machines (operating some previously unknown types of API) into our platform. To allow doing so, we have to negotiate a callback format that would allow us to call partners' APIs and expose two new endpoints providing the following capabilities:

      -
    • registering new API types in the system;
    • -
    • providing the list of the coffee machines and their API types;
    • +
    • Registering new API types in the system
    • +
    • Providing the list of the coffee machines and their API types.

    For example, we might provide a second API family (the partner-bound one) with the following methods:

    // 1. Register a new API type
     PUT /v1/api-types/{api_type}
     {
    -    "order_execution_endpoint": {
    -        // Callback function description
    -    }
    +  "order_execution_endpoint": {
    +    // Callback function description
    +  }
     }
     
    // 2. Provide a list of coffee machines
     // with their API types
     PUT /v1/partners/{partnerId}/coffee-machines
     {
    -    "coffee_machines": [{
    -        "api_type",
    -        "location",
    -        "supported_recipes"
    -    }, …]
    +  "coffee_machines": [{
    +    "api_type",
    +    "location",
    +    "supported_recipes"
    +  }, …]
     }
     
    -

    So the mechanics are like that:

    +

    So the mechanics are like this:

      -
    • a partner registers their API types, coffee machines, and supported recipes;
    • -
    • with each incoming order, our server will call the callback function, providing the order data in the stipulated format.
    • +
    • A partner registers their API types, coffee machines, and supported recipes.
    • +
    • With each incoming order, our server will call the callback function, providing the order data in the stipulated format.
    -

    Now the partners might dynamically plug their coffee machines in and get the orders. But we now will do the following exercise:

    +

    Now the partners might dynamically plug their coffee machines in and get the orders. But now we will do the following exercise:

      -
    • enumerate all the implicit assumptions we have made;
    • -
    • enumerate all the implicit coupling mechanisms we need to have the platform functioning properly.
    • +
    • Enumerate all the implicit assumptions we have made
    • +
    • Enumerate all the implicit coupling mechanisms we need to have the platform functioning properly.
    -

    It may look like there are no such things in our API since it's quite simple and basically just describes making some HTTP call — but that's not true.

    +

    It may seem like there are no such things in our API since it's quite simple and basically just describes making some HTTP calls, but that's not true.

    1. It is implied that every coffee machine supports every order option like varying the beverage volume.
    2. -
    3. There is no need to display some additional data to the end-user regarding coffee being brewed on these new coffee machines.
    4. +
    5. There is no need to display additional data to the end-user regarding coffee being brewed on these new coffee machines.
    6. The price of the beverage doesn't depend on the selected partner or coffee machine type.
    -

    We have written down this list having one purpose in mind: we need to understand, how exactly will we make these implicit arrangements explicit if we need that. For example, if different coffee machines provide different functionality — let's say, some of them are capable of brewing fixed beverage volumes only — what would change in our API?

    -

    The universal approach to making such amendments is: to consider the existing interface as a reduction of some more general one like if some parameters were set to defaults and therefore omitted. So making a change is always a three-step process:

    +

    We have written down this list having one purpose in mind: we need to understand how exactly we will make these implicit arrangements explicit if we need to. For example, if different coffee machines provide different functionality — let's say, some of them are capable of brewing fixed beverage volumes only — what would change in our API?

    +

    The universal approach to making such amendments is to consider the existing interface as a reduction of some more general one, as if some parameters were set to defaults and therefore omitted. So making a change is always a three-step process:

    1. Explicitly define the programmatical contract as it works right now.
    2. -
    3. Extend the functionality: add a new method allowing for tackling those restrictions set in the previous paragraph.
    4. -
    5. Pronounce the existing interfaces (those defined in #1) being “helpers” to new ones (those defined in #2) which sets some options to default values.
    6. +
    7. Extend the functionality: add a new method that allows for tackling the restrictions set in the previous paragraph.
    8. +
    9. Pronounce the existing interfaces (those defined in #1) as “helpers” to the new ones (those defined in #2) that pre-fill some options with default values.
    -

    More specifically, if we talk about changing available order options, we should do the following.

    +

    More specifically, if we talk about changing available order options, we should do the following:

    1. Describe the current state. All coffee machines, plugged via the API, must support three options: sprinkling with cinnamon, changing the volume, and contactless delivery.

    2. -

      Add new “with-options” endpoint:

      +

      Add a new “with options” endpoint:

      PUT /v1/partners/{partner_id}⮠
         /coffee-machines-with-options
       {
      @@ -4032,18 +4032,18 @@ PUT /v1/partners/{partnerId}/coffee-machines
       
    3. -

      Pronounce PUT /coffee-machines endpoint as it now stands in the protocol being equivalent to calling PUT /coffee-machines-with-options if we pass those three options to it (sprinkling with cinnamon, changing the volume, contactless delivery) and therefore being a partial case — a helper to a more general call.

      +

      Pronounce the PUT /coffee-machines endpoint as it currently stands in the protocol as equivalent to calling PUT /coffee-machines-with-options if we pass those three options to it (sprinkling with cinnamon, changing the volume, contactless delivery) and therefore being a partial case — a helper to a more general call.

    Usually, just adding a new optional parameter to the existing interface is enough; in our case, adding non-mandatory options to the PUT /coffee-machines endpoint.

    -

    NB. When we talk about defining the contract as it works right now, we're talking about internal agreements. We must have asked partners to support those three options while negotiating the interaction format. If we had failed to do so from the very beginning, and now are defining these in a course of expanding the public API, it's a very strong claim to break backward compatibility, and we should never do that (see the previous chapter).

    +

    NB. When we talk about defining the contract as it works right now, we're referring to internal agreements. We must have asked partners to support those three options while negotiating the interaction format. If we had failed to do so from the very beginning and are now defining them during the expansion of the public API, it's a very strong claim to break backward compatibility, and we should never do that (see the previous chapter).

    Limits of Applicability

    -

    Though this exercise looks very simple and universal, its consistent usage is possible only if the hierarchy of entities is well-designed from the very beginning and, which is more important, the vector of the further API expansion is clear. Imagine that after some time passed, the options list got new items; let's say, adding syrup or a second espresso shot. We are totally capable of expanding the list — but not the defaults. So the “default” PUT /coffee-machines interface will eventually become totally useless because the default set of three options will not only be any longer of use but will also look ridiculous: why these three options, what are the selection criteria? In fact, the defaults and the method list will be reflecting the historical stages of our API development, and that's totally not what you'd expect from the helpers and defaults nomenclature.

    -

    Alas, this dilemma can't be easily resolved. On one hand, we want developers to write neat and laconic code, so we must provide useful helpers and defaults. On the other hand, we can't know in advance which sets of options will be the most useful after several years of expanding the API.

    -

    NB. We might mask this problem in the following manner: one day gather all these oddities and re-define all the defaults with one single parameter. For example, introduce a special method like POST /use-defaults {"version": "v2"} which would overwrite all the defaults with more suitable values. That will ease the learning curve, but your documentation will become even worse after that.

    -

    In the real world, the only viable approach to somehow tackle the problem is the weak entity coupling, which we will discuss in the next chapter.

    Chapter 29. Strong Coupling and Related Problems 

    -

    To demonstrate the strong coupling problematics let us move to really interesting things. Let's continue our “variation analysis”: what if the partners wish to offer not only the standard beverages but their own unique coffee recipes to end-users? The catch is that the partner API as we described it in the previous chapter does not expose the very existence of the partner network to the end user, and thus describes a simple case. Once we start providing methods to alter the core functionality, not just API extensions, we will soon face next-level problems.

    -

    So, let us add one more endpoint for registering the partner's own recipe:

    +

    Though this exercise appears to be simple and universal, its consistent usage is only possible if the hierarchy of entities is well-designed from the very beginning and, more importantly, if the direction of further API expansion is clear. Imagine that after some time has passed, the options list has new items, such as adding syrup or a second espresso shot. We are fully capable of expanding the list, but not the defaults. As a result, the “default” PUT /coffee-machines interface will eventually become completely useless because the default set of three options will no longer be useful and will appear ridiculous: why these three options, what are the selection criteria? In fact, the defaults and the method list reflect the historical stages of our API development, which is not what one would expect from the helpers and defaults nomenclature.

    +

    Alas, this dilemma can't be easily resolved. On one hand, we want developers to write neat and concise code, so we must provide useful helpers and defaults. On the other hand, we can't know in advance which sets of options will be the most useful after several years of API evolution.

    +

    NB. We might mask this problem in the following manner: one day gather all these oddities and re-define all the defaults with a single parameter. For example, introduce a special method like POST /use-defaults {"version": "v2"} that would overwrite all the defaults with more suitable values. This would ease the learning curve, but it would make your documentation even worse.

    +

    In the real world, the only viable approach to somehow tackle the problem is weak entity coupling, which we will discuss in the next chapter.

    Chapter 29. Strong Coupling and Related Problems 

    +

    To demonstrate the problems of strong coupling, let's move on to interesting topics. Let's continue our “variation analysis”: what if partners wish to offer their own unique coffee recipes to end users in addition to the standard beverages? The challenge is that the partner API, as described in the previous chapter, does not expose the very existence of the partner network to the end user, thus presenting a simple case. However, once we start providing methods to modify the core functionality, not just API extensions, we will soon face next-level problems.

    +

    So, let's add one more endpoint for registering the partner's own recipe:

    // Adds new recipe
     POST /v1/recipes
     {
    @@ -4053,17 +4053,17 @@ POST /v1/recipes
         "description",
         "default_volume"
         // Other properties to describe
    -    // the beverage to end-user
    +    // the beverage to an end user
         …
       }
     }
     
    -

    At first glance, again, it looks like a reasonably simple interface, explicitly decomposed into abstraction levels. But let us imagine the future — what would happen with this interface when our system evolves further?

    -

    The first problem is obvious to those who read the “Describing Final Interfaces” chapter thoroughly: product properties must be localized. That will lead us to the first change:

    +

    At first glance, this appears to be a reasonably simple interface, explicitly decomposed into abstraction levels. But let's imagine the future and consider what would happen to this interface as our system evolves further.

    +

    The first problem is obvious to those who thoroughly read the “Describing Final Interfaces” chapter: product properties must be localized. This leads us to the first change:

    "product_properties": {
       // "l10n" is the standard abbreviation
       // for "localization"
    -  "l10n" : [{
    +  "l10n": [{
         "language_code": "en", 
         "country_code": "US", 
         "name", 
    @@ -4071,19 +4071,19 @@ POST /v1/recipes
       }, /* other languages and countries */ … ]
     }
     
    -

    And here the first big question arises: what should we do with the default_volume field? From one side, that's an objective property measured in standardized units, and it's being passed to the program execution engine. On the other side, in countries like the United States, we had to specify beverage volumes not like “300 ml,” but “10 fl oz.” We may propose two solutions:

    +

    And here arises the first big question: what should we do with the default_volume field? On one hand, it's an objective property measured in standardized units to be passed to the program execution engine. On the other hand, in countries like the United States, beverage volumes are specified as “10 fl oz” rather than “300 ml.” We can propose two solutions:

      -
    • either the partner provides the corresponding number only, and we will make readable descriptions on our own behalf,
    • -
    • or the partner provides both the number and all of its localized representations.
    • +
    • Either the partner provides only the corresponding number and we will make readable descriptions ourselves, or
    • +
    • The partner provides both the number and all its localized representations.
    -

    The flaw in the first option is that a partner might be willing to use the service in some new country or language — and will be unable to do so until the API supports them. The flaw in the second option is that it works with predefined volumes only, so you can't order an arbitrary beverage volume. So the very first step we've made effectively has us trapped.

    -

    The localization flaws are not the only problem with this API. We should ask ourselves a question — why do we really need these name and description? They are simply non-machine-readable strings with no specific semantics. At first glance, we need them to return them back in the /v1/search method response, but that's not a proper answer: why do we really return these strings from search?

    -

    The correct answer lies a way beyond this specific interface. We need them because some representation exists. There is a UI for choosing beverage type. Probably the name and description fields are simply two designations of the beverage for a user to read, a short one (to be displayed on the search results page) and a long one (to be displayed in the extended product specification block). It actually means that we set the requirements to the API based on some specific design. But what if a partner is making their own UI for their own app? Not only they might not actually need two descriptions, but we are also deceiving them. The name is not “just a name”, it implies some restrictions: it has recommended length which is optimal to some specific UI, and it must look consistently on the search results page. Indeed, the “our best quality™ coffee” or “Invigorating Morning Freshness®” designations would look very weird in-between “Cappuccino,” “Lungo,” and “Latte.”

    -

    There is also another side to this story. As UIs (both ours' and partners') tend to evolve, new visual elements will be eventually introduced. For example, a picture of the beverage, its energy value, allergen information, etc. The product_properties entity will become a scrapyard for tons of optional fields, and learning how setting what field results in what effects in the UI will be an interesting quest, full of probes and mistakes.

    -

    The problems we're facing are the problems of strong coupling. Each time we offer an interface like described above, we in fact prescript implementing one entity (recipe) based on implementations of other entities (UI layout, localization rules). This approach disrespects the very basic principle of the “top to bottom” API design because low-level entities must not define high-level ones.

    +

    The flaw in the first option is that a partner might be willing to use the service in a new country or language, but they will be unable to do so until the API is localized to support these new territories. The flaw in the second option is that it only works with predefined volumes, so ordering an arbitrary beverage volume will not be possible. The very first step we've taken effectively has had us trapped.

    +

    The localization flaws are not the only problem with this API. We should ask ourselves a question: why do we really need these name and description fields? They are simply non-machine-readable strings with no specific semantics. At first glance, we need them to return in the /v1/search method response, but that's not a proper answer as it only leads to another question: why do we actually return these strings from search?

    +

    The correct answer lies beyond this specific interface. We need them because some representation exists. There is a UI for choosing a beverage type. The name and description fields are probably two designations of the beverage for the user to read, a short one (to be displayed on the search results page) and a long one (to be displayed in the extended product specification block). This means we are setting the API requirements based on some specific visual design. But what if a partner is creating their own UI for their own app? Not only might they not actually need two descriptions, but we are also deceiving them. The name is not “just a name” as it implies certain restrictions: it has a recommended length that is optimal for a specific UI, and it must look consistent on the search results page. Indeed, designations like “our best quality™ coffee” or “Invigorating Morning Freshness®” would look out of place among “Cappuccino,” “Lungo,” and “Latte.”

    +

    There is also another aspect to consider. As UIs (both ours and partners') evolve, new visual elements will eventually be introduced. For example, a picture of the beverage, its energy value, allergen information, etc. The product_properties entity will become a scrapyard for numerous optional fields, and learning how to set each field and its effects in the UI will be an interesting journey filled with trial and error.

    +

    The problems we're facing are the problems of strong coupling. Each time we offer an interface as described above, we effectively dictate the implementation of one entity (recipe) based on the implementations of other entities (UI layout, localization rules). This approach disregards the fundamental principle of “top to bottom” API design because low-level entities should not define high-level ones.

    The Rule of Contexts

    -

    To make things worse, let us state that the inverse principle is also correct: high-level entities must not define low-level ones as well, 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 the interfaces for adding a new recipe 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 noted a localization context. There is some set of languages and regions we support in our API, and there are the requirements — what exactly partners 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, either internally or within an SDK:

    +

    To exacerbate matters, let us state that the inverse principle is also true: high-level entities should not define low-level ones as well since it is not their responsibility. The way out of this logical labyrinth is that high-level entities should define a context for other objects to interpret. To properly design the interfaces for adding a new recipe we should not attempt to find a better data format. Instead, we need to understand the explicit and implicit contexts that exist in our subject area.

    +

    We have already identified a localization context. There is a set of languages and regions supported by our API, and there are requirements for what partners must provide to make the API work in a new region. Specifically, there must be a formatting function to represent beverage volume somewhere in our API code, either internally or within an SDK:

    l10n.volume.format = function(
       value, language_code, country_code
     ) { … }
    @@ -4096,7 +4096,7 @@ POST /v1/recipes
       ) → '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 through the partner API. Like this:

    +

    To ensure our API works correctly with a new language or region, the partner must either define this function or indicate which pre-existing implementation to use through the partner API, like this:

    // Add a general formatting rule
     // for the Russian language
     PUT /formatters/volume/ru
    @@ -4108,8 +4108,8 @@ PUT /formatters/volume/ru
     // in the “US” region
     PUT /formatters/volume/ru/US
     {
    -  // in the US, we need to recalculate
    -  // the number, then add a postfix
    +  // In the US, we need to recalculate
    +  // the number and add a postfix
       "value_transform": {
         "action": "divide",
         "divisor": 30
    @@ -4117,15 +4117,15 @@ PUT /formatters/volume/ru/US
       "template": "{volume} ун."
     }
     
    -

    so the above-mentioned l10n.volume.format function implementation might retrieve the formatting rules for the new language-region pair and use them.

    -

    NB: we are more than aware that such a simple format isn't enough to cover real-world localization use cases, and one either relies on existing libraries or designs a sophisticated format for such templating, which takes into account such things as grammatical cases and rules of rounding numbers up or allow defining formatting rules in a form of function code. The example above is simplified for purely educational purposes.

    -

    Let us deal with the name and description problem then. To lower the coupling level there, we need to formalize (probably just to ourselves) a “layout” concept. We are asking for providing the name and description fields not because we just need them, but for representing them in some specific user interface. This specific UI might have an identifier or a semantic name.

    +

    so the aforementioned l10n.volume.format function implementation can retrieve the formatting rules for the new language-region pair and utilize them.

    +

    NB: we are well aware that such a simple format is not sufficient to cover real-world localization use cases, and one would either rely on existing libraries or design a sophisticated format for such templating, which takes into account various aspects such as grammatical cases and rules for rounding numbers or allows defining formatting rules in the form of function code. The example above is simplified for purely educational purposes.

    +

    Let's address the name and description problem. To reduce the coupling level, we need to formalize (probably just for ourselves) a “layout” concept. We request the provision of the name and description fields not because we theoretically need them but to present them in a specific user interface. This particular UI might have an identifier or a semantic name associated with it:

    GET /v1/layouts/{layout_id}
     {
       "id",
    -  // We would probably have lots of layouts,
    -  // so it's better to enable extensibility
    -  // from the beginning
    +  // Since we will likely have numerous
    +  // layouts, it's better to enable 
    +  // extensibility from the beginning
       "kind": "recipe_search",
       // Describe every property we require
       // to have this layout rendered properly
    @@ -4133,11 +4133,11 @@ PUT /formatters/volume/ru/US
         // Since we learned that `name`
         // is actually a title for a search
         // result snippet, it's much more
    -    // convenient to have explicit
    +    // convenient to have an explicit
         // `search_title` instead
         "field": "search_title",
         "view": {
    -      // Machine-readable description
    +      // A machine-readable description
           // of how this field is rendered
           "min_length": "5em",
           "max_length": "20em",
    @@ -4151,20 +4151,20 @@ PUT /formatters/volume/ru/US
       ]
     }
     
    -

    So the partner may decide, which option better suits them. They can provide mandatory fields for the standard layout:

    +

    Thus, the partner can decide which option better suits their needs. They can provide mandatory fields for the standard layout:

    PUT /v1/recipes/{id}/properties/l10n/{lang}
     {
       "search_title", "search_description"
     }
     
    -

    or create a layout of their own and provide the data fields it requires, or they may ultimately design their own UI and don't use this functionality at all, defining neither layouts nor corresponding data fields.

    -

    Then our interface would ultimately look like this:

    +

    Alternatively, they can create their own layout and provide the data fields it requires, or they may choose to design their own UI and not use this functionality at all, thereby defining neither layouts nor corresponding data fields.

    +

    Ultimately, our interface would look like this:

    POST /v1/recipes
     { "id" }
     →
     { "id" }
     
    -

    This conclusion might look highly counter-intuitive, but lacking any fields in a Recipe simply tells us that this entity possesses no specific semantics of its own, and is simply an identifier of a context; a method to point out where to look for the data needed by other entities. In the real world, we should implement a builder endpoint capable of creating all the related contexts with a single request:

    +

    This conclusion might seem highly counter-intuitive, but the absence of fields in a Recipe simply tells us that this entity possesses no specific semantics of its own. It serves solely as an identifier of a context, a way to indicate where to find the data needed by other entities. In the real world, we should implement a builder endpoint capable of creating all the related contexts with a single request:

    POST /v1/recipe-builder
     {
       "id",
    @@ -4173,11 +4173,11 @@ PUT /formatters/volume/ru/US
         "default_volume",
         "l10n"
       },
    -  // Create all the desirable layouts
    +  // Create all the desired layouts
       "layouts": [{
         "id", "kind", "properties"
       }],
    -  // Add all the formatters needed
    +  // Add all the required formatters
       "formatters": {
         "volume": [
           { 
    @@ -4190,12 +4190,13 @@ PUT /formatters/volume/ru/US
           }
         ]
       },
    -  // Other actions needed to be done
    -  // to register new recipe in the system
    +  // Other actions needed
    +  // to register a new recipe 
    +  // in the system
       …
     }
     
    -

    We should also note that providing a newly created entity identifier by the requesting side isn't exactly the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, we're risking getting lots of collisions on recipe names used by different partners, so we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the “Describing Final Interfaces” chapter.

    +

    We should also note that providing a newly created entity identifier from the requesting side is not the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, there is a risk of encountering collisions with recipe names used by different partners. Therefore, we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the “Describing Final Interfaces” chapter.

    POST /v1/recipes/custom
     {
       // The first part of the composite
    @@ -4212,20 +4213,20 @@ PUT /formatters/volume/ru/US
     }
     

    Also note that this format allows us to maintain an important extensibility point: different partners might have both shared and isolated namespaces. Furthermore, we might introduce special namespaces (like common, for example) to allow editing standard recipes (and thus organizing our own recipes backoffice).

    -

    NB: a mindful reader might have noted that this technique was already used in our API study much earlier in the “Separating Abstraction Levels” chapter with regards to the “program” and “program run” entities. Indeed, we might do it without the program-matcher endpoint and make it this way:

    +

    NB: a mindful reader might have noticed that this technique was already used in our API study much earlier in the “Separating Abstraction Levels” chapter regarding the “program” and “program run” entities. Indeed, we can propose an interface for retrieving commands to execute a specific recipe without the program-matcher endpoint, and instead, do it this way:

    GET /v1/recipes/{id}/run-data/{api_type}
     →
    -{ /* A description, how to
    +{ /* A description of how to
          execute a specific recipe
          using a specified API type */ }
     
    -

    Then developers would have to make this trick to get coffee prepared:

    +

    Then developers would have to make this trick to get the beverage prepared:

      -
    • learn the API type of the specific coffee machine;
    • -
    • get the execution description, as stated above;
    • -
    • depending on the API type, run some specific commands.
    • +
    • Learn the API type of the specific coffee machine.
    • +
    • Retrieve the execution description as described above.
    • +
    • Based on the API type, execute specific commands.
    -

    Obviously, such an interface is absolutely unacceptable, simply because in the majority of use cases developers don't care at all, which API type the specific coffee machine runs. To avoid the necessity of introducing such bad interfaces we created a new “program” entity, which constitutes merely a context identifier, just like a “recipe” entity does. A program_run_id entity is also organized in this manner, it also possesses no specific properties, being just a program run identifier.

    Chapter 30. Weak Coupling 

    +

    Obviously, such an interface is completely unacceptable because, in the majority of use cases, developers do not care at all about which API type the specific coffee machine exposes. To avoid the need for introducing such poor interfaces we created a new “program” entity, which serves solely as a context identifier, just like a “recipe” entity does. Similarly, the program_run_id entity is also organized in the same manner, without possessing any specific properties and representing just a program run identifier.

    Chapter 30. Weak Coupling 

    In the previous chapter, we've demonstrated how breaking strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. But let us return to the question we have previously mentioned in the “Extending through Abstracting” chapter: how should we parametrize the order preparation process implemented via a third-party API? In other words, what is the order_execution_endpoint required in the API type registration handler?

    PUT /v1/api-types/{api_type}
     {
    @@ -4262,8 +4263,8 @@ PUT /formatters/volume/ru/US
     

    Now let's try to imagine a real-world example that doesn't fit into our “three imperatives to rule them all” picture. That's quite easy as well: what if we're plugging not a coffee house, but a vending machine via our API? From one side, it means that the modify endpoint and all related stuff are simply meaningless: the contactless takeout requirement means nothing to a vending machine. On the other side, the machine, unlike the people-operated café, requires takeout approval: the end-user places an order while being somewhere in some other place then walks to the machine and pushes the “get the order” button in the app. We might, of course, require the user to stand up in front of the machine when placing an order, but that would contradict the entire product concept of users selecting and ordering beverages and then walking to the takeout point.

    Programmable takeout approval requires one more endpoint, let's say, program_takeout_endpoint. And so we've lost our way in a forest of five endpoints:

      -
    • to have vending machines integrated a partner must implement the program_takeout_endpoint, but doesn't need the program_modify_endpoint;
    • -
    • to have regular coffee houses integrated a partner must implement the program_modify_endpoint, but doesn't need the program_takeout_endpoint.
    • +
    • To have vending machines integrated a partner must implement the program_takeout_endpoint, but doesn't need the program_modify_endpoint.
    • +
    • To have regular coffee houses integrated a partner must implement the program_modify_endpoint, but doesn't need the program_takeout_endpoint.

    Furthermore, we have to describe both endpoints in the docs. It's quite natural that the takeout endpoint is very specific; unlike requesting contactless delivery, which we hid under the pretty general modify endpoint, operations like takeout approval will require introducing a new unique method every time. After several iterations, we would have a scrapyard, full of similarly looking methods, mostly optional — but developers would need to study the docs nonetheless to understand, which methods are needed in your specific situation, and which are not.

    NB: in this example, we assumed that passing program_takeout_endpoint parameter is the flag to the application to display the “get the order” button; it would be better to add something like a supported_flow field to the PUT /api-types/ endpoint to provide an explicit flag instead of this implicit convention; however, this wouldn't change the problematics of stockpiling optional methods in the interface, so we skipped it to keep examples laconic.

    @@ -4271,14 +4272,14 @@ PUT /formatters/volume/ru/US

    It is also worth mentioning that we unwittingly violated the abstraction levels isolation principle. At the vending machine API level, there is no such thing as a “contactless takeout,” that's actually a product concept.

    So, how would we tackle this issue? Using one of two possible approaches: either thoroughly study the entire subject area and its upcoming improvements for at least several years ahead, or abandon strong coupling in favor of a weak one. How would the ideal solution look to both parties? Something like this:

      -
    • the higher-level program API level doesn't actually know how the execution of its commands works; it formulates the tasks at its own level of understanding: brew this recipe, send user's requests to a partner, allow the user to collect their order;
    • -
    • the underlying program execution API level doesn't care what other same-level implementations exist; it just interprets those parts of the task that make sense to it.
    • +
    • The higher-level program API level doesn't actually know how the execution of its commands works; it formulates the tasks at its own level of understanding: brew this recipe, send user's requests to a partner, allow the user to collect their order.
    • +
    • The underlying program execution API level doesn't care what other same-level implementations exist; it just interprets those parts of the task that make sense to it.

    If we take a look at the principles described in the previous chapter, we would find that this principle was already formulated: we need to describe informational contexts at every abstraction level and design a mechanism to translate them between levels. Furthermore, in a more general sense, we formulated it as early as in “The Data Flow” paragraph of the “Separating Abstraction Levels” chapter.

    In our case we need to implement the following mechanisms:

      -
    • running a program creates a corresponding context comprising all the essential parameters;
    • -
    • there is the information stream regarding the state modifications: the execution level may read the context, learn about all the changes and report back the changes of its own.
    • +
    • Running a program creates a corresponding context comprising all the essential parameters.
    • +
    • There is the information stream regarding the state modifications: the execution level may read the context, learn about all the changes and report back the changes of its own.

    There are different techniques to organize this data flow, but, basically, we always have two contexts and a two-way data pipe in between. If we were developing an SDK, we would express the idea with emitting and listening events, like this:

    /* Partner's implementation of the program
    @@ -4317,15 +4318,15 @@ registerProgramRunHandler(
     

    NB: In the case of HTTP API, a corresponding example would look rather bulky as it would require implementing several additional endpoints for the message exchange like GET /program-run/events and GET /partner/{id}/execution/events. We would leave this exercise to the reader. Also, it's worth mentioning that in real-world systems such event queues are usually organized using external event messaging systems like Apache Kafka or Amazon SNS/SQS.

    At this point, a mindful reader might begin protesting because if we take a look at the nomenclature of the new entities, we will find that nothing changed in the problem statement. It actually became even more complicated:

      -
    • instead of calling the takeout method, we're now generating a pair of takeout_requested / takeout_ready events;
    • -
    • instead of a long list of methods that shall be implemented to integrate the partner's API, we now have a long list of context entities and events they generate;
    • -
    • and with regards to technological progress, we've changed nothing: now we have deprecated fields and events instead of deprecated methods.
    • +
    • Instead of calling the takeout method, we're now generating a pair of takeout_requested / takeout_ready events
    • +
    • Instead of a long list of methods that shall be implemented to integrate the partner's API, we now have a long list of context entities and events they generate
    • +
    • And with regards to technological progress, we've changed nothing: now we have deprecated fields and events instead of deprecated methods.

    And this remark is totally correct. Changing API formats doesn't solve any problems related to the evolution of functionality and underlying technology. Changing API formats serves another purpose: to make the code written by developers stay clean and maintainable. Why would strong-coupled integration (i.e., making entities interact via calling methods) render the code unreadable? Because both sides are obliged to implement the functionality which is meaningless in their corresponding subject areas. Code that integrates vending machines into the system must respond “ok” to the contactless delivery request — so after a while, these implementations would comprise a handful of methods that just always return true (or false).

    The difference between strong coupling and weak coupling is that the field-event mechanism isn't obligatory for both actors. Let us remember what we sought to achieve:

      -
    • a higher-level context doesn't know how low-level API works — and it really doesn't; it describes the changes that occur within the context itself, and reacts only to those events that mean something to it;
    • -
    • a low-level context doesn't know anything about alternative implementations — and it really doesn't; it handles only those events which mean something at its level and emits only those events that could happen under its specific conditions.
    • +
    • A higher-level context doesn't know how low-level API works — and it really doesn't; it describes the changes that occur within the context itself, and reacts only to those events that mean something to it.
    • +
    • A low-level context doesn't know anything about alternative implementations — and it really doesn't; it handles only those events which mean something at its level and emits only those events that could happen under its specific conditions.

    It's ultimately possible that both sides would know nothing about each other and wouldn't interact at all, and this might happen with the evolution of underlying technologies.

    NB: in the real world this might not be the case as we might want the application to know, whether the takeout request was successfully served or not, i.e., listen to the takeout_ready event and require the takeout_ready flag in the state of the execution context. Still, the general possibility of not caring about the implementation details is a very powerful technique that makes the application code much less complex — of course, unless this knowledge is important to the user.

    @@ -4369,8 +4370,8 @@ registerProgramRunHandler(

    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 bothering ourselves with weak coupling because we expect alternative implementations of the lower abstraction level to pop up. Situations with different realizations of higher abstraction levels emerging are, of course, possible, but quite rare. The tree of alternative implementations usually grows from root to leaves.

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

      -
    • if the technical level is under change, that must not affect product qualities and the code written by partners;
    • -
    • if the product is changing, e.g., we start selling flight tickets instead of preparing coffee, there is literally no sense to preserve backward compatibility at technical abstraction levels. Ironically, we may actually make our API sell tickets instead of brewing coffee without breaking backward compatibility, but the partners' code will still become obsolete.
    • +
    • If the technical level is under change, that must not affect product qualities and the code written by partners.
    • +
    • If the product is changing, e.g., we start selling flight tickets instead of preparing coffee, there is literally no sense to preserve backward compatibility at technical abstraction levels. Ironically, we may actually make our API sell tickets instead of brewing coffee without breaking backward compatibility, but the partners' code will still become obsolete.

    In conclusion, as higher-level APIs are evolving more slowly and much more consistently than low-level APIs, reverse strong coupling might often be acceptable or even desirable, at least from the price-quality ratio point of view.

    NB: many contemporary frameworks explore a shared state approach, Redux being probably the most notable example. In the Redux paradigm, the code above would look like this:

    @@ -4429,12 +4430,12 @@ ProgramContext.dispatch = (action) => {

    Then we would have come to the understanding that a “search result” is actually a composition of two interfaces:

    • -

      when we create an order, we need the search result to provide those fields which describe the order itself; it might be a structure like:

      +

      When we create an order, we need the search result to provide those fields which describe the order itself; it might be a structure like:

      {coffee_machine_id, recipe_id, volume, currency_code, price},

      -

      or we can encode this data in the single offer_id;

      +

      or we can encode this data in the single offer_id.

    • -

      to have this search result displayed in the app, we need a different data set: name, description, and formatted and localized prices.

      +

      To have this search result displayed in the app, we need a different data set: name, description, and formatted and localized prices.

    So our interface (let us call it ISearchResult) is actually a composition of two other interfaces: IOrderParameters (an entity that allows for creating an order) and ISearchItemViewParameters (some abstract representation of the search result in the UI). This interface split should automatically lead us to additional questions:

    @@ -4451,9 +4452,9 @@ ProgramContext.dispatch = (action) => {
    1. Remember the Iceberg's Waterline

    If you haven't given any formal guarantee, it doesn't mean that you can violate informal ones. Often, just fixing bugs in APIs might render some developers' code inoperable. We might illustrate it with a real-life example that the author of this book has actually faced once:

      -
    • there was an API to place a button into a visual container; according to the docs, it was taking its position (offsets to the container's corner) as a mandatory argument;
    • -
    • in reality, there was a bug: if the position was not supplied, no exception was thrown; buttons were simply stacked in the corner one after another;
    • -
    • after the error had been fixed, we got a bunch of complaints: clients did really use this flaw to stack the buttons in the container's corner.
    • +
    • There was an API to place a button into a visual container. According to the docs, it was taking its position (offsets to the container's corner) as a mandatory argument.
    • +
    • In reality, there was a bug: if the position was not supplied, no exception was thrown. Buttons were simply stacked in the corner one after another.
    • +
    • After the error had been fixed, we got a bunch of complaints: clients did really use this flaw to stack the buttons in the container's corner.

    If fixing an error might somehow affect real customers, you have no other choice but to emulate this erroneous behavior until the next major release. This situation is quite common if you develop a large API with a huge audience. For example, operating systems developers literally have to transfer old bugs to new OS versions.

    2. Test the Formal Interface
    @@ -4465,26 +4466,26 @@ ProgramContext.dispatch = (action) => {
    3. Isolate the Dependencies

    In the case of a gateway API that provides access to some underlying API or aggregates several APIs behind a single façade, there is a strong temptation to proxy the original interface as is, thus not introducing any changes to it and making life much simpler by sparing an effort needed to implement the weak-coupled interaction between services. For example, while developing program execution interfaces as described in the “Separating Abstraction Levels” chapter we might have taken the existing first-kind coffee-machine API as a role model and provided it in our API by just proxying the requests and responses as is. Doing so is highly undesirable because of several reasons:

      -
    • usually, you have no guarantees that the partner will maintain backward compatibility or at least keep new versions more or less conceptually akin to the older ones;
    • -
    • any partner's problem will automatically ricochet into your customers.
    • +
    • Usually, you have no guarantees that the partner will maintain backward compatibility or at least keep new versions more or less conceptually akin to the older ones.
    • +
    • Any partner's problem will automatically ricochet into your customers.

    The best practice is quite the opposite: isolate the third-party API usage, i.e., develop an abstraction level that will allow for:

      -
    • keeping backward compatibility intact because of extension capabilities incorporated in the API design;
    • -
    • negating partner's problems by technical means: +
    • Keeping backward compatibility intact because of extension capabilities incorporated in the API design
    • +
    • Negating partner's problems by technical means:
        -
      • limiting the partner's API usage in case of load surges;
      • -
      • implementing the retry policies or other methods of recovering after failures;
      • -
      • caching some data and states to have the ability to provide some (at least partial) functionality even if the partner's API is fully unreachable;
      • -
      • finally, configuring an automatic fallback to another partner or alternative API.
      • +
      • Limiting the partner's API usage in case of load surges
      • +
      • Implementing retry policies or other methods of recovering after failures
      • +
      • Caching some data and states to have the ability to provide some (at least partial) functionality even if the partner's API is fully unreachable
      • +
      • Finally, configuring an automatic fallback to another partner or alternative API.
    4. Implement Your API Functionality Atop Public Interfaces

    There is an antipattern that occurs frequently: API developers use some internal closed implementations of some methods which exist in the public API. It happens because of two reasons:

      -
    • often the public API is just an addition to the existing specialized software, and the functionality, exposed via the API, isn't being ported back to the closed part of the project, or the public API developers simply don't know the corresponding internal functionality exists;
    • -
    • in the course of extending the API, some interfaces become abstract, but the existing functionality isn't affected; imagine that while implementing the PUT /formatters interface described in the “Strong Coupling and Related Problems” chapter API developers have created a new, more general version of the volume formatter but hasn't changed the implementation of the existing one, so it continues working for pre-existing languages.
    • +
    • Often the public API is just an addition to the existing specialized software, and the functionality, exposed via the API, isn't being ported back to the closed part of the project, or the public API developers simply don't know the corresponding internal functionality exists.
    • +
    • In the course of extending the API, some interfaces become abstract, but the existing functionality isn't affected. Imagine that while implementing the PUT /formatters interface described in the “Strong Coupling and Related Problems” chapter API developers have created a new, more general version of the volume formatter but hasn't changed the implementation of the existing one, so it continues working for pre-existing languages.

    There are obvious local problems with this approach (like the inconsistency in functions' behavior, or the bugs which were not found while testing the code), but also a bigger one: your API might be simply unusable if a developer tries any non-mainstream approach, because of performance issues, bugs, instability, etc., as the API developers themselves never tried to use this public interface for anything important.

    NB. The perfect example of avoiding this anti-pattern is the development of compilers; usually, the next compiler's version is compiled with the previous compiler's version.

    @@ -4846,13 +4847,13 @@ X-ApiName-Partner-Id: <partner_id>

    NB: we're deliberately skipping many nuances of the standard:

    • -

      a caching key might be composite (i.e., include request headers) if the response contains the Vary header.

      +

      A caching key might be composite (i.e., include request headers) if the response contains the Vary header.

    • -

      an idempotency key might also be composite if the request contains the Range header.

      +

      An idempotency key might also be composite if the request contains the Range header.

    • -

      if there are no explicit cache control headers, the caching policy will not be defined by the HTTP verb alone. It will also depend on the response status code, other request and response headers, and platform policies.

      +

      If there are no explicit cache control headers, the caching policy will not be defined by the HTTP verb alone. It will also depend on the response status code, other request and response headers, and platform policies.

      To keep the chapter size reasonable, we will not discuss these details, but we highly recommend reading the standard thoroughly.

    @@ -5045,9 +5046,9 @@ Authorization: Bearer <token>

    To properly develop the API product, you must be able to answer exactly this question: why would your customers prefer making some actions programmatically? It's not an idle question: out of this book's author's experience, the product owners' lack of expertise in working with APIs exactly is the largest problem of API product development.

    End users interact with your API indirectly, through applications built upon it by software engineers acting on behalf of some company (and sometimes there is more than one engineer in between you and an end user). From this point of view, the API's target audience resembles a Maslow-like pyramid:

      -
    • users constitute the pyramid's base; they look for the fulfillment of their needs and don't think about technicalities;
    • -
    • business owners form a middle level; they match users' needs against technical capabilities declared by developers and build products;
    • -
    • developers make up the pyramid's apex; it is developers who work with APIs directly, and they decide which of the competing APIs to choose.
    • +
    • Users constitute the pyramid's base; they look for the fulfillment of their needs and don't think about technicalities.
    • +
    • Business owners form a middle level; they match users' needs against technical capabilities declared by developers and build products.
    • +
    • Developers make up the pyramid's apex; it is developers who work with APIs directly, and they decide which of the competing APIs to choose.

    The obvious conclusion of this model is that you must advertise the advantages of your API to developers. They will select the technology, and business owners will translate the concept to end users. If former or acting developers manage the API product, they often tend to evaluate the API market competitiveness in this dimension only and mainly channel the product promotion efforts to the developers' auditory.

    “Stop!” the mindful reader must yell at this moment. “The actual order of things is exactly the opposite!”

    @@ -5095,18 +5096,19 @@ Authorization: Bearer <token>
    • The API doesn't cover integration use cases well:
        -
      • internal customers employ quite a specific technological stack, and the API is poorly optimized to work with other programming languages / operating systems / frameworks;
      • -
      • for external customers, the learning curve will be pretty flat as they can't take a look at the source code or talk to the API developers directly, unlike internal customers that are much more familiar with the API concepts;
      • -
      • documentation often covers only some subset of use cases needed by internal customers;
      • -
      • the API services ecosystem which we will describe in “The API Services Range” chapter usually doesn't exist.
      • +
      • Internal customers employ quite a specific technological stack, and the API is poorly optimized to work with other programming languages / operating systems / frameworks.
      • +
      • For external customers, the learning curve will be pretty flat as they can't take a look at the source code or talk to the API developers directly, unlike internal customers that are much more familiar with the API concepts.
      • +
      • Documentation often covers only some subset of use cases needed by internal customers.
      • +
      • The API services ecosystem which we will describe in “The API Services Range” chapter usually doesn't exist.
    • Any resources spent are directed to covering internal customer needs first. It means the following:
        -
      • API development plans are totally opaque to partners, and sometimes look just absurd with obvious problems being neglected for years;
      • -
      • technical support of external customers is financed on leftovers.
      • +
      • API development plans are totally opaque to partners, and sometimes look just absurd with obvious problems being neglected for years.
      • +
      • Technical support of external customers is financed on leftovers.
    • +
    • Often, developers of internal services break backward compatibility or issue new major versions whenever they need it and don't care about the consequences of these decisions for external API partners.

    All those problems lead to having an external API that actually hurts the company's reputation, not improves it. You're providing a very bad service for a very critical and skeptical auditory. If you don't have a resource to develop the API as a product for external customers, better don't even start.

    5. API = an Advertisement Site
    @@ -5115,9 +5117,9 @@ Authorization: Bearer <token>

    If an API has neither explicit nor implicit monetization, it might still generate some income, increasing the company's brand awareness through displaying logos and other recognizable elements in partners' apps, either native (if the API goes with UI elements) or agreed-upon ones (if partners are obliged to embed specific branding in those places where the API functionality is used or the data acquired through API is displayed). The API provider company's goals in this case are either attracting users to the company's services or just increasing brand awareness in general. [In the case of our coffee example, let's imagine that our main business is something totally unrelated to the coffee machine APIs, like selling tires, and by providing the API we hope to increase brand recognition and get a reputation as an IT company.]

    The target audiences for such self-promotion might also differ:

      -
    • you might seek to increase brand awareness among end users (by embedding logos and links to your services on partner's websites and applications), and even build the brand exclusively through such integrations [for example if our coffee API provides coffeeshop ratings, and we're working hard on making consumers demand the coffeeshops to publish the ratings];
    • -
    • you might concentrate efforts on increasing awareness among business owners [for example, for partners integrating a coffee ordering widget on their websites to also pay attention to your tires catalog];
    • -
    • finally, you might provide APIs only to make developers know your company's name to increase their knowledge of your other products or just to improve your reputation as an employer (this activity is sometimes called “tech-PR”).
    • +
    • You might seek to increase brand awareness among end users (by embedding logos and links to your services on partner's websites and applications), and even build the brand exclusively through such integrations [for example if our coffee API provides coffeeshop ratings, and we're working hard on making consumers demand the coffeeshops to publish the ratings].
    • +
    • You might concentrate efforts on increasing awareness among business owners [for example, for partners integrating a coffee ordering widget on their websites to also pay attention to your tires catalog].
    • +
    • Finally, you might provide APIs only to make developers know your company's name to increase their knowledge of your other products or just to improve your reputation as an employer (this activity is sometimes called “tech-PR”).

    Additionally, we might talk about forming a community, i.e., a network of developers (or customers, or business owners) who are loyal to the product. The benefits of having such a community might be substantial: lowering the technical support costs, getting a convenient channel for publishing announcements regarding new services and new releases, and obtaining beta users for upcoming products.

    7. API = a Feedback and UGC Tool
    @@ -5143,15 +5145,15 @@ Authorization: Bearer <token>

    The above-mentioned fragmentation of the API target audience, i.e., the “developers — business — end users” triad, makes API product management quite a non-trivial problem. Yes, the basics are the same: find your auditory's needs and satisfy them; the problem is that your product has several different audiences, and their interests sometimes diverge. The end users' request for an affordable cup of coffee does not automatically imply business demand for a coffee machine API.

    Generally speaking, the API product vision must include the same three elements:

      -
    • grasping how end users would like to have their problems solved;
    • -
    • projecting how businesses would solve those problems if appropriate tools existed;
    • -
    • understanding what technical solutions for developers might exist to help them implement the functionality businesses would ask for, and where are the boundaries of their applicability.
    • +
    • Grasping how end users would like to have their problems solved
    • +
    • Projecting how businesses would solve those problems if appropriate tools existed
    • +
    • Understanding what technical solutions for developers might exist to help them implement the functionality businesses would ask for, and where are the boundaries of their applicability.

    In different markets and different situations, the “weight” of each element differs. If you're creating an API-first product for developers with no UI components, you might skip the end users' problems analysis; and, by contrast, if you're providing an API to extremely valuable functionality and you're holding a close-to-monopolistic position on the market, you might actually never care about how developers love your software architecture or how convenient your interfaces are for them — as they simply have no other choice.

    Still, in the majority of cases, we have to deal with two-step heuristics based on either technical capabilities or business demands:

      -
    • you might first form the vision of how you might help business owners given the technical capabilities you have (heuristics step one); then, the general vision of how your API will be used to satisfy end users' needs (heuristics step two);
    • -
    • or, given your understanding of business owners' problems, you might make one heuristic “step right” to outline future functionality for end users and one “step left” to evaluate possible technical solutions.
    • +
    • You might first form the vision of how you might help business owners given the technical capabilities you have (heuristics step one). Then, the general vision of how your API will be used to satisfy end users' needs (heuristics step two), or
    • +
    • Given your understanding of business owners' problems, you might make one heuristic “step right” to outline future functionality for end users and one “step left” to evaluate possible technical solutions.

    As both approaches are still heuristic, the API product vision is inevitably fuzzy, and it's rather normal: if you could have got a full and clear understanding of what end-user products might be developed on top of your API, you might have developed them on your own behalf, skipping intermediary agents. It is also important to keep in mind that many APIs pass the “terraforming” stage (see the previous chapter) thus preparing the ground for new markets and new types of services — so your idealistic vision of a nearby future where delivering freshly brewed coffee by drones will be a norm of life is to be refined and clarified while new companies providing new kinds of services are coming to the market. (Which in its turn will make an impact on the monetization model: detailing the countenance of the forthcoming will make your abstract KPIs and theoretical benefits of having an API more and more concrete.)

    The same fuzziness should be kept in mind while making interviews and getting feedback. Software engineers will mainly report the problems they've got with the technical integrations, and rarely speak of business-related issues; meanwhile, business owners care little about the inconvenience of writing code. Both will have some knowledge regarding the end users' problems, but it's usually limited to the market segment the partner operates on.

    @@ -5165,56 +5167,50 @@ Authorization: Bearer <token>

    As we have described in the previous chapters, managing an API product requires building relations with both business partners and developers. (Ideally, with end users as well; though this option is seldom available to API providers.)

    Let's start with developers. The specifics of software engineers as an auditory are the following:

      -
    • -

      developers are highly-educated individuals with practical thinking; as a rule, they choose technical products with extreme rationality (unless you're giving them cool backpacks with fancy prints for free);

      +
    • Developers are highly-educated individuals with practical thinking; as a rule, they choose technical products with extreme rationality (unless you're giving them cool backpacks with fancy prints for free).
        -
      • this doesn't prevent them from having a certain aptitude towards, let's say, specific programming languages or frameworks; however, affecting those aptitudes is extremely hard and is normally not in the API vendor's power;
      • +
      • This doesn't prevent them from having a certain aptitude towards, let's say, specific programming languages or frameworks; however, affecting those aptitudes is extremely hard and is normally not in the API vendor's power.
    • -
    • -

      in particular, developers are quite skeptical towards promotional materials and overstatements and are ready to actually check whether your claims are true;

      -
    • -
    • -

      it is very hard to communicate to them via regular marketing channels; they get information from highly specialized communities, and they stick to opinions proved by concrete numbers and examples (ideally, code samples);

      +
    • In particular, developers are quite skeptical towards promotional materials and overstatements and are ready to actually check whether your claims are true.
    • +
    • It is very hard to communicate to them via regular marketing channels; they get information from highly specialized communities, and they stick to opinions proved by concrete numbers and examples (ideally, code samples).
        -
      • the “influencers” words are not very valuable to them, as no opinions are trusted if unsubstantiated;
      • +
      • The “influencers” words are not very valuable to them, as no opinions are trusted if unsubstantiated.
    • -
    • -

      the Open Source and free software ideas are widespread among developers; if you try to make money out of things that must be free and/or open from their point of view (for example, by proclaiming interfaces an intellectual property), you will face resistance (and views on this “musts”… differ).

      -
    • +
    • The Open Source and free software ideas are widespread among developers If you try to make money out of things that must be free and/or open from their point of view (for example, by proclaiming interfaces an intellectual property), you will face resistance (and views on this “musts”… differ).

    Because of the above-mentioned specifics (first of all, the relative insignificance of influencers and the critical attitude towards promotions), you will have to communicate to developers via very specific media:

      -
    • collective blogs (like the “r/programming” subreddit or dev.to)
    • +
    • Collective blogs (like the “r/programming” subreddit or dev.to)
    • Q&A sites (StackOverflow, Experts Exchange)
    • -
    • educational services (CodeAcademy, Udemy)
    • -
    • technical conferences and webinars.
    • +
    • Educational services (CodeAcademy, Udemy)
    • +
    • Technical conferences and webinars.

    In all these channels, the direct advertising of your API is either problematic or impossible. (Well, strictly speaking, you may buy the banner on one of the sites advertising the advantages of your API, but we hardly doubt it will improve your relations with developers.) You need to generate some valuable and/or interesting content for them, which will improve the knowledge of your API. And this is the job for your developers: writing articles, answering questions, recording webinars, and giving pitches.

    Developers do like sharing the experience, and will probably be eager to do it — during their work hours. A proper conference talk, let alone an educational course, requires a lot of preparation time. Out of this book's author's experience, two things are crucial for tech-PR:

      -
    • incentives, even nominal ones — the job of promoting a product should be rewarded;
    • -
    • methodicalness and quality standards — you might actually do the content review just like you do the code review.
    • +
    • Incentives, even nominal ones — the job of promoting a product should be rewarded
    • +
    • Methodicalness and quality standards — you might actually do the content review just like you do the code review.

    Nothing could make the worse counter-advertising for your product than a poorly prepared pitch (as we said, the mistakes will be inevitably found and pointed to) or a badly camouflaged commercial in a form of a pitch (the reason is actually the same). Texts are to be worked upon: pay attention to the structure, logic, and tempo of the narration. Even a technical story must be finely constructed; after it's ended, the listeners must have a clear understanding of what idea you wanted to communicate (and it'd rather be somehow coupled with your API's fitness for their needs).

    A word on “evangelists” (those are people who have some credibility in the IT community and work on promoting a technology or a tech company, being a company's contractor or even a staff member, effectively carrying out all those above-mentioned activities like blog-posting, course-preparing, conference-speaking, etc.) Having an evangelist makes the API development team exempt from the necessity of performing the tech-PR. However, we would rather advise having this expertise inside the team, as direct interaction with developers helps with forming the product vision. (That doesn't mean the evangelists are not needed at all - you might well combine these two strategies.)

    Open Source

    The important question which sooner or later will stand in any API vendor's way is making the source code open. This decision has both advantages and disadvantages:

      -
    • you will improve the knowledge of the brand, and some respect will be paid to you by the IT community; +
    • You will improve the knowledge of the brand, and some respect will be paid to you by the IT community.
        -
      • that's given your code is finely written and commented;
      • +
      • That's given your code is finely written and commented.
    • -
    • you will get some additional feedback — ideally, pull requests from third-party developers; +
    • You will get some additional feedback — ideally, pull requests from third-party developers
        -
      • and you will also get a number of inquiries and comments ranging from useless to obviously provocative ones, to which you will have to respond politely;
      • +
      • And you will also get a number of inquiries and comments ranging from useless to obviously provocative ones, to which you will have to respond politely.
    • -
    • donating code to open source makes developers trust the company more, and affects their readiness to rely on the platform; +
    • Donating code to open source makes developers trust the company more, and affects their readiness to rely on the platform.
        -
      • but it also increases risks, both from the information security point of view and from the product one, as a dissatisfied community might fork your repo and create a competing product.
      • +
      • But it also increases risks, both from the information security point of view and from the product one, as a dissatisfied community might fork your repo and create a competing product.
    @@ -5223,8 +5219,8 @@ Authorization: Bearer <token>

    There is one very important addition to the discourse: as informational technologies are universally in great demand, a significant percentage of your customers will not be professional software engineers. A huge number of people are somewhere on the track of mastering the occupation: someone is trying to write code in addition to the basic duties, another one is being retrained now, and the third one is studying the basics of computer science on their own. Many of those non-professional developers make a direct impact on the process of selecting an API vendor — for example, small business owners who additionally seek to automate some routine tasks programmatically.

    It will be more correct if we say that you're actually working for two main types of audiences:

      -
    • beginners and amateurs, for whom each of those integration tasks would be completely new and unexplored territory;
    • -
    • professional developers who possess vast experience in integrating different third-party systems.
    • +
    • Beginners and amateurs, for whom each of those integration tasks would be completely new and unexplored territory
    • +
    • Professional developers who possess vast experience in integrating different third-party systems.

    This fact greatly affects everything we had discussed previously (except for, maybe, open-sourcing, as amateur developers pay little attention to it):

      @@ -5235,8 +5231,8 @@ Authorization: Bearer <token>

      Finally, it's almost impossible in a course of a single product to create an API that will fit well both amateur and professional developers: the former need the maximum simplicity of implementing basic use cases, while the latter seek the ability to adapt the API to match technological stack and development paradigms, and the problems they solve usually require deep customization. We will discuss the matter in “The API Services Range” chapter.

      Chapter 55. Communicating with Business Owners 

      The basics of interacting with business partners are to some extent paradoxically contrary to the basics of communicating with developers:

        -
      • on one hand, partners are much more loyal and sometimes even enthusiastic regarding opportunities you offer (especially free ones);
      • -
      • on the other hand, communicating the meaning of your offer to the business owners is much more complicated than conveying it to developers, as it's generally hard to explain what are the advantages of integrating via APIs (as a concept).
      • +
      • On one hand, partners are much more loyal and sometimes even enthusiastic regarding opportunities you offer (especially free ones).
      • +
      • On the other hand, communicating the meaning of your offer to the business owners is much more complicated than conveying it to developers, as it's generally hard to explain what are the advantages of integrating via APIs (as a concept).

      After all, working with business auditory essentially means lucidly explaining the characteristics and the advantages of the product. In that sense, API “sells” just like any other kind of software.

      As a rule, the farther some industry sector from information technologies is, the more enthusiastic its representatives about your API features are, and the less is the chance that this enthusiasm will be converted into a real integration. The one thing that should help the case is extensive work with the developer community (see the previous chapter) that will result in establishing a circle of freelancers and outsourcers eager to help non-IT businesses with integrations. You might help in developing this market by creating educational courses and issuing certificates proving the bearer's skills in working with your API (or some broader layer of technology).

      @@ -5246,10 +5242,10 @@ Authorization: Bearer <token>

      Usually, any functionality available through an API might be split into independent units. For example, in our coffee API, there are offer search endpoints and order processing endpoints. Nothing could prevent us from either pronouncing those functional clusters different APIs or, vice versa, considering them as parts of one API.

      Different companies employ different approaches to determining the granularity of API services, i.e., what is counted as a separate product and what is not. To some extent, this is a matter of convenience and taste judgment. Consider splitting an API into parts if:

        -
      • it makes sense for partners to integrate only one API part, i.e., there are some isolated subsets of the API that alone provide enough means to solve users' problems;
      • -
      • API parts might be versioned separately and independently, and it is meaningful from the partners' point of view (this usually means that those “isolated” APIs are either fully independent or maintain strict backward compatibility and introduce new major versions only when it's absolutely necessary; otherwise, maintaining a matrix which API #1 version is compatible with which API #2 version will soon become a catastrophe);
      • -
      • it makes sense to set tariffs and limits for each API service independently;
      • -
      • the auditory of the API segments (either developers, business owners, or end users) is not overlapping, and “selling” granular API to customers is much easier than aggregated.
      • +
      • It makes sense for partners to integrate only one API part, i.e., there are some isolated subsets of the API that alone provide enough means to solve users' problems.
      • +
      • API parts might be versioned separately and independently, and it is meaningful from the partners' point of view (this usually means that those “isolated” APIs are either fully independent or maintain strict backward compatibility and introduce new major versions only when it's absolutely necessary; otherwise, maintaining a matrix which API #1 version is compatible with which API #2 version will soon become a catastrophe).
      • +
      • It makes sense to set tariffs and limits for each API service independently.
      • +
      • The auditory of the API segments (either developers, business owners, or end users) is not overlapping, and “selling” granular API to customers is much easier than aggregated.

      NB: still, those split APIs might still be a part of a united SDK, to make programmers' lives easier.

      Vertical Scaling of API Services

      @@ -5277,37 +5273,33 @@ Authorization: Bearer <token>

      The obvious key performance indicator (KPI) #1 is the number of end users and the number of integrations (i.e., partners using the API). Normally, they are in some sense a business health barometer: if there is a normal competitive situation among the API suppliers, and all of them are more or less in the same position, then the figure of how many developers (and consequently, how many end users) are using the API is the main metric of success of the API product.

      However, sheer numbers might be deceiving, especially if we talk about free-to-use integrations. There are several factors that make them less reliable:

        -
      • -

        the high-level API services that are meant for point-and-click integration (see the previous chapter) are significantly distorting the statistics, especially if the competitors don't provide such services; typically, for one full-scale integration there will be tens, maybe hundreds, of those lightweight embedded widgets;

        +
      • The high-level API services that are meant for point-and-click integration (see the previous chapter) are significantly distorting the statistics, especially if the competitors don't provide such services; typically, for one full-scale integration there will be tens, maybe hundreds, of those lightweight embedded widgets.
          -
        • thereby, it's crucial to have partners counted for each kind of the integration independently;
        • +
        • Thereby, it's crucial to have partners counted for each kind of the integration independently.
      • -
      • -

        partners tend to use the API in suboptimal ways:

        +
      • Partners tend to use the API in suboptimal ways:
          -
        • embed it at every website page / application screen instead of only those where end users can really interact with the API;
        • -
        • put widgets somewhere deep in the page / screen footer, or hide it behind spoilers;
        • -
        • initialize a broad range of API modules, but use only a limited subset of them;
        • +
        • Embed it at every website page / application screen instead of only those where end users can really interact with the API
        • +
        • Put widgets somewhere deep in the page / screen footer, or hide it behind spoilers
        • +
        • Initialize a broad range of API modules, but use only a limited subset of them.
      • -
      • -

        the greater the API auditory is, the less the number of unique visitors means as at some moment the penetration will be close to 100%; for example, a regular Internet user interacts with Google or Facebook counters, well, every minute, so the daily audience of those API fundamentally cannot be increased further.

        -
      • +
      • The greater the API auditory is, the less the number of unique visitors means as at some moment the penetration will be close to 100%; for example, a regular Internet user interacts with Google or Facebook counters, well, every minute, so the daily audience of those API fundamentally cannot be increased further.

      All the abovementioned problems naturally lead us to a very simple conclusion: not only the raw numbers of users and partners are to be gauged, but their engagement as well, i.e., the target actions (such as searching, observing some data, interacting with widgets) shall be determined and counted. Ideally, these target actions must correlate with the API monetization model:

        -
      • if the API is monetized through displaying ads, then the user's activity towards those ads (e.g., clicks, interactions) is to be measured;
      • -
      • if the API attracts customers to the core service, then count the transitions;
      • -
      • if the API is needed for collecting feedback and gathering UGC, then calculate the number of reviews left and entities edited.
      • +
      • If the API is monetized through displaying ads, then the user's activity towards those ads (e.g., clicks, interactions) is to be measured.
      • +
      • If the API attracts customers to the core service, then count the transitions.
      • +
      • If the API is needed for collecting feedback and gathering UGC, then calculate the number of reviews left and entities edited.

      Additionally, the functional KPIs are often employed: how frequently some API features are used. (Also, it helps with prioritizing further API improvements.) In fact, that's still measuring target actions, but those that are made by developers, not end users. It's rather complicated to gather the usage data for software libraries and frameworks, though still doable (however, you must be extremely cautious with that, as any auditory rather nervously reacts to finding that some statistic is gathered automatically).

      The most complicated case is that of API being a tool for (tech)PR and (tech)marketing. In this case, there is a cumulative effect: increasing the API audience doesn't momentarily bring any profit to the company. First, you got a loyal developer community, then this reputation helps you to hire people. First, your company's logo flashes on third-party webpages and applications, then the top-of-mind brand knowledge increases. There is no direct method of evaluating how some action (let's say, a new release or an event for developers) affects the target metrics. In this case, you have to operate indirect metrics, such as the audience of the documentation site, the number of mentions in the relevant communication channels, the popularity of your blogs and seminars, etc.

      Let us summarize the paragraph:

        -
      • counting direct metrics such as the total number of users and partners is a must and is totally necessary for moving further, but that's not a proper KPI;
      • -
      • the proper KPI should be formulated based on the number of target actions that are made through the platform;
      • -
      • the definition of target action depends on the monetization model and might be quite straightforward (like the number of paying partners, or the number of paid ad clicks) or, to the contrary, pretty implicit (like the growth of the company's developer blog auditory).
      • +
      • Counting direct metrics such as the total number of users and partners is a must and is totally necessary for moving further, but that's not a proper KPI.
      • +
      • The proper KPI should be formulated based on the number of target actions that are made through the platform.
      • +
      • The definition of target action depends on the monetization model and might be quite straightforward (like the number of paying partners, or the number of paid ad clicks) or, to the contrary, pretty implicit (like the growth of the company's developer blog auditory).

      SLA

      This chapter would be incomplete if we didn't mention the “hygienic” KPI — the service level and the service availability. We won't be describing the concept in detail, as the API SLA isn't any different from any other digital services SLAs. Let us just state that this metric must be tracked, especially if we talk about pay-to-use APIs. However, in many cases, API vendors prefer to offer rather loose SLAs, treating the provided functionality as a data access or content licensing service.

      @@ -5317,26 +5309,26 @@ Authorization: Bearer <token>

      Comparing to Competitors

      While measuring KPIs of any service, it's important not only to evaluate your own numbers but also to match them against the state of the market:

        -
      • what is your market share, and how is it evolving over time?
      • -
      • is your service growing faster than the market itself or is the rate the same, or is it even less?
      • -
      • what proportion of the growth is caused by the growth of the market, and what is related to your efforts?
      • +
      • What is your market share, and how is it evolving over time?
      • +
      • Is your service growing faster than the market itself or is the rate the same, or is it even less?
      • +
      • What proportion of the growth is caused by the growth of the market, and what is related to your efforts?

      Getting answers to those questions might be quite non-trivial in the case of API services. Indeed, how could you learn how many integrations has your competitor had during the same period of time, and what number of target actions had happened on their platform? Sometimes, the providers of popular analytical tools might help you with this, but usually, you have to monitor the potential partners' apps and websites and gather the statistics regarding APIs they're using. The same applies to market research: unless your niche is significant enough for some analytical company to conduct a study, you will have to either commission such work or make your own estimations — conversely, through interviewing potential customers.

      Chapter 58. Identifying Users and Preventing Fraud 

      In the context of working with an API, we talk about two kinds of users of the system:

        -
      • users-developers, i.e., your partners writing code atop of the API;
      • -
      • end users interacting with applications implemented by the users-developers.
      • +
      • Users-developers, i.e., your partners writing code atop of the API
      • +
      • End users interacting with applications implemented by the users-developers.

      In most cases, you need to have both of them identified (in a technical sense: discern one unique customer from another) to have answers to the following questions:

        -
      • how many users are interacting with the system (simultaneously, daily, monthly, and yearly)?
      • -
      • how many actions does each user make?
      • +
      • How many users are interacting with the system (simultaneously, daily, monthly, and yearly)?
      • +
      • How many actions does each user make?

      NB. Sometimes, when an API is very large and/or abstract, the chain linking the API vendor to end users might comprise more than one developer as large partners provide services implemented atop of the API to the smaller ones. You need to count both direct and “derivative” partners.

      Gathering this data is crucial because of two reasons:

        -
      • to understand the system's limits and to be capable of planning its growth;
      • -
      • to understand the number of resources (ultimately, money) that are spent (and gained) on each user.
      • +
      • To understand the system's limits and to be capable of planning its growth
      • +
      • To understand the number of resources (ultimately, money) that are spent (and gained) on each user.

      In the case of commercial APIs, the quality and timeliness of gathering this data are twice that important, as the tariff plans (and therefore the entire business model) depend on it. Therefore, the question of how exactly we're identifying users is crucial.

      Identifying Applications and Their Owners

      @@ -5353,14 +5345,14 @@ Authorization: Bearer <token>

      Mobile applications might be conveniently tracked through their identifiers in the corresponding store (Google Play, App Store, etc.), so it makes sense to require this identifier to be passed by partners as an API initialization parameter. Websites with some degree of confidence might be identified by the Referer and Origin HTTP headers.

      This data is not itself reliable, but it allows for making cross-checks:

        -
      • if a key was issued for one specific domain but requests are coming with a different Referer, it makes sense to investigate the situation and maybe ban the possibility to access the API with this Referer or this key;
      • -
      • if an application initializes API by providing a key registered to another application, it makes sense to contact the store administration and ask for removing one of the apps.
      • +
      • If a key was issued for one specific domain but requests are coming with a different Referer, it makes sense to investigate the situation and maybe ban the possibility to access the API with this Referer or this key.
      • +
      • If an application initializes API by providing a key registered to another application, it makes sense to contact the store administration and ask for removing one of the apps.

      NB: don't forget to set infinite limits for using the API with the localhost, 127.0.0.1 / [::1] Referers, and also for your own sandbox if it exists. Yes, abusers will sooner or later learn this fact and will start exploiting it, but otherwise, you will ban local development and your own website much sooner than that.

      The general conclusion is:

        -
      • it is highly desirable to have partners formally identified (either through obtaining API keys or by providing contact data such as website domain or application identifier in a store while initializing the API);
      • -
      • this information shall not be trusted unconditionally; there must be double-checking mechanisms that identify suspicious requests.
      • +
      • It is highly desirable to have partners formally identified (either through obtaining API keys or by providing contact data such as website domain or application identifier in a store while initializing the API).
      • +
      • This information shall not be trusted unconditionally; there must be double-checking mechanisms that identify suspicious requests.

      Identifying End Users

      Usually, you can put forward some requirements for self-identifying of partners, but asking end users to reveal contact information is impossible in most cases. All the methods of measuring the audience described below are imprecise and often heuristic. (Even if partner application functionality is only available after registration and you do have access to that profile data, it's still a game of assumptions, as an individual account is not the same as an individual user: several different persons might use a single account, or, vice versa, one person might register many accounts.) Also, note that gathering this sort of data might be legally regulated (though we will be mostly speaking about anonymized data, there might still be some applicable law).

      @@ -5370,9 +5362,9 @@ Authorization: Bearer <token>

      If the API is provided as a server-to-server one, there will be no access to the end user's IP address. However, it makes sense to require partners to propagate the IP address (for example, in a form of the X-Forwarded-For header) — among other things, to help partners fight fraud and unintended usage of the API.

      Until recently, IP addresses were also a convenient statistics indicator because it was quite expensive to get a large pool of unique addresses. However, with ipv6 advancement this restriction is no longer actual; ipv6 rather put the light on the fact that you can't just count unique addresses — the aggregates are to be tracked:

        -
      • the cumulative number of requests by networks, i.e., the hierarchical calculations (the number of /8, /16, /24, etc. networks)
      • -
      • the cumulative statistics by autonomous networks (AS);
      • -
      • the API requests through known public proxies and TOR network.
      • +
      • The cumulative number of requests by networks, i.e., the hierarchical calculations (the number of /8, /16, /24, etc. networks)
      • +
      • The cumulative statistics by autonomous networks (AS)
      • +
      • The API requests through known public proxies and TOR network.

      An abnormal number of requests in one network might be evidence of the API being actively used inside some corporative environment (or NATs being widespread in the region).

      @@ -5385,9 +5377,9 @@ Authorization: Bearer <token>

      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 comprises three steps:

        -
      • identifying suspicious users;
      • -
      • optionally, asking for an additional authentication factor;
      • -
      • making decisions and applying access restrictions.
      • +
      • Identifying suspicious users
      • +
      • Optionally, asking for an additional authentication factor
      • +
      • Making decisions and applying access restrictions.
      1. Identifying Suspicious Users

      Generally speaking, there are two approaches we might take, the static one and the dynamic (behavioral) one.

      @@ -5402,10 +5394,10 @@ Authorization: Bearer <token>
      3. Restricting Access

      The illusion of having a broad choice of technical means of identifying fraud users should not deceive you as you will soon discover the lack of effective methods of restricting those users. Banning them by cookie / Referer / User-Agent makes little to no impact as this data is supplied by clients, and might be easily forged. In the end, you have four mechanisms for suppressing illegal activities:

        -
      • banning users by IP (networks, autonomous systems)
      • -
      • requiring mandatory user identification (maybe tiered: login / login with confirmed phone number / login with confirmed identity / login with confirmed identity and biometrics / etc.)
      • -
      • returning fake responses
      • -
      • filing administrative abuse reports.
      • +
      • Banning users by IP (networks, autonomous systems)
      • +
      • Requiring mandatory user identification (maybe tiered: login / login with confirmed phone number / login with confirmed identity / login with confirmed identity and biometrics / etc.)
      • +
      • Returning fake responses
      • +
      • Filing administrative abuse reports.

      The problem with the first option is the collateral damage you will inflict, especially if you have to ban subnets.

      The second option, though quite rational, is usually inapplicable to real APIs, as not every partner will agree with the approach, and definitely not every end user. This will also require being compliant with the existing personal data laws.

      @@ -5423,26 +5415,16 @@ Authorization: Bearer <token>
    • Allowing partners to restrict the functionality available under specific API keys:

        -
      • -

        setting the allowed IP address range for server-to-server APIs, allowed Referers and application ids for client APIs;

        -
      • -
      • -

        white-listing only allowed API functions for a specific key;

        -
      • -
      • -

        other restrictions that make sense in your case (in our coffee API example, it's convenient to allow partners to prohibit API calls outside of countries and cities they work in).

        -
      • +
      • Setting the allowed IP address range for server-to-server APIs, allowed Referers and application ids for client APIs
      • +
      • White-listing only allowed API functions for a specific key
      • +
      • Other restrictions that make sense in your case (in our coffee API example, it's convenient to allow partners to prohibit API calls outside of countries and cities they work in).
    • Introducing additional request signing:

        -
      • -

        for example, if on the partner's website, there is a form displaying the best lungo offers, for which the partners call the API endpoint like /v1/search?recipe=lungo&api_key={apiKey}, then the API key might be replaced with a signature like sign = HMAC("recipe=lungo", apiKey); the signature might be stolen as well, but it will be useless for malefactors as they will be able to find only lungo with it;

        -
      • -
      • -

        instead of API keys, time-based one-time passwords (TOTP) might be used; these tokens are valid for a short period of time only (typically, one minute), which makes using stolen keys much more complicated.

        -
      • +
      • For example, if on the partner's website, there is a form displaying the best lungo offers, for which the partners call the API endpoint like /v1/search?recipe=lungo&api_key={apiKey}, then the API key might be replaced with a signature like sign = HMAC("recipe=lungo", apiKey). The signature might be stolen as well, but it will be useless for malefactors as they will be able to find only lungo with it.
      • +
      • Instead of API keys, time-based one-time passwords (TOTP) might be used. These tokens are valid for a short period of time only (typically, one minute), which makes using stolen keys much more complicated.
    • @@ -5467,8 +5449,8 @@ Authorization: Bearer <token>

      The first two cases are actually consequences of product-wise or technical flaws in the API development, and they should be avoided. The third case differs little from supporting end users of the UGC service itself.

      If we talk about supporting partners, it's revolving around two major topics:

        -
      • legal and administrative support with regard to the terms of service and the SLA (and that's usually about responding to business owners' inquiries);
      • -
      • helping developers with technical issues.
      • +
      • Legal and administrative support with regard to the terms of service and the SLA (and that's usually about responding to business owners' inquiries)
      • +
      • Helping developers with technical issues.

      The former is of course extremely important for any healthy service (including APIs) but again bears little API-related specifics. In the context of this book, we are much more interested in the latter.

      As an API is a program product, developers will be in fact asking how this specific piece of code that they have written works. This fact raises the level of required customer support staff members' expertise quite high as you need a software engineer to read the code and understand the problem. But this is but half of the problem; another half is, as we have mentioned in the previous chapters, that most of these questions will be asked by inexperienced or amateur developers. In a case of a popular API, it means that 9 out of 10 inquiries will not be about the API. Less skilled developers lack language knowledge, their experience with the platform is fragmented, and they can't properly formulate their problem (and therefore search for an answer on the Internet before contacting support; though, let us be honest, they usually don't even try).

      @@ -5492,12 +5474,8 @@ Authorization: Bearer <token>
    • As a rule, developers are totally not happy about the perspective of coping with incoming requests and answering them. The first line of support will still let through a lot of dilettante or badly formulated questions, and that will annoy on-duty API developers. There are several approaches to mitigate the problem:

        -
      • -

        try to find people with a customer-oriented mindset, who like this activity, and encourage them (including financial stimulus) to perform support functions; it might be someone on the team (and not necessarily a developer) or some active community member;

        -
      • -
      • -

        the remaining load must be distributed among the developers equally and fairly, up to introducing the duty calendar.

        -
      • +
      • Try to find people with a customer-oriented mindset, who like this activity, and encourage them (including financial stimulus) to perform support functions; it might be someone on the team (and not necessarily a developer) or some active community member.
      • +
      • The remaining load must be distributed among the developers equally and fairly, up to introducing the duty calendar.
    • @@ -5524,37 +5502,37 @@ Authorization: Bearer <token>
      2. Code Samples

      From the above-mentioned, it's obvious that code samples are a crucial tool to acquire and retain new API users. It's extremely important to choose examples that help newcomers to start working with the API. Improper example selection will greatly reduce the quality of your documentation. While assembling the set of code samples, it is important to follow the rules:

        -
      • examples must cover actual API use cases: the better you guess the most frequent developers' needs, the more friendly and straightforward your API will look to them;
      • -
      • examples must be laconic and atomic: mixing a bunch of tricks in one code sample dramatically reduces its readability and applicability;
      • -
      • examples must be close to real-world app code; the author of this book once faced a situation when a synthetic code sample, totally meaningless in the real world, was mindlessly replicated by developers in abundance.
      • +
      • Examples must cover actual API use cases: the better you guess the most frequent developers' needs, the more friendly and straightforward your API will look to them.
      • +
      • Examples must be laconic and atomic: mixing a bunch of tricks in one code sample dramatically reduces its readability and applicability.
      • +
      • Examples must be close to real-world app code. The author of this book once faced a situation when a synthetic code sample, totally meaningless in the real world, was mindlessly replicated by developers in abundance.

      Ideally, examples should be linked to all other kinds of documentation, e.g., the reference might contain code samples relevant to the entity being described.

      3. Sandboxes

      Code samples will be much more useful to developers if they are “live,” i.e., provided as editable pieces of code that might be modified and executed. In the case of library APIs, the online sandbox featuring a selection of code samples will suffice, and existing online services like JSFiddle might be used. With other types of APIs, developing sandboxes might be much more complicated:

        -
      • if the API provides access to some data, then the sandbox must allow working with a real dataset, either a developer's own one (e.g., bound to their user profile) or some test data;
      • -
      • if the API provides an interface, visual or programmatic, to some non-online environment, like UI libs for mobile devices do, then the sandbox itself must be an emulator or a simulator of that environment, in a form of an online service or a standalone app.
      • +
      • If the API provides access to some data, then the sandbox must allow working with a real dataset, either a developer's own one (e.g., bound to their user profile) or some test data.
      • +
      • If the API provides an interface, visual or programmatic, to some non-online environment, like UI libs for mobile devices do, then the sandbox itself must be an emulator or a simulator of that environment, in a form of an online service or a standalone app.
      4. Tutorial

      A tutorial is a specifically written human-readable text describing some concepts of working with the API. A tutorial is something in-between a reference and examples. It implies some learning, more thorough than copy-pasting code samples, but requires less time investment than reading the whole reference.

      A tutorial is a sort of “book” that you write to explain to the reader how to work with your API. So, a proper tutorial must follow book-writing patterns, i.e., explain the concepts coherently and consecutively chapter after chapter. Also, a tutorial must provide:

        -
      • general knowledge of the subject area; for example, a tutorial for cartographical APIs must explain trivia regarding geographical coordinates and working with them;
      • -
      • proper API usage scenarios, i.e., the “happy paths”;
      • -
      • proper reactions to program errors that could happen;
      • -
      • detailed studies on advanced API functionality (with detailed examples).
      • +
      • General knowledge of the subject area; for example, a tutorial for cartographical APIs must explain trivia regarding geographical coordinates and working with them
      • +
      • Proper API usage scenarios, i.e., the “happy paths”
      • +
      • Proper reactions to program errors that could happen
      • +
      • Detailed studies on advanced API functionality (with detailed examples).

      Usually, a tutorial comprises a common section (basic terms and concepts, notation keys) and a set of sections regarding each functional domain exposed via the API. Frequently, tutorials contain a “Quick Start” (“Hello, world!”) section: the smallest possible code sample that would allow developers to build a small app atop the API. “Quick Starts” aim to cover two needs:

        -
      • to provide a default entry-point, the easiest to understand and the most useful text for those who heard about your API for the first time;
      • -
      • to engage developers, to make them touch the service by a mean of a real-world example.
      • +
      • To provide a default entry-point, the easiest to understand and the most useful text for those who heard about your API for the first time
      • +
      • To engage developers, to make them touch the service by a mean of a real-world example.

      Also, “Quick starts” are a good indicator of how exactly well did you do your homework of identifying the most important use cases and providing helper methods. If your Quick Start comprises more than ten lines of code, you have definitely done something wrong.

      5. Frequently Asked Questions and Knowledge Bases

      After you publish the API and start supporting users (see the previous chapter) you will also accumulate some knowledge of what questions are asked most frequently. If you can't easily integrate answers into the documentation, it's useful to compile a specific “Frequently Asked Questions” (aka FAQ) article. A FAQ article must meet the following criteria:

        -
      • address the real questions (you might frequently find FAQs that were reflecting not users' needs, but the API owner's desire to repeat some important information once more; it's useless, or worse — annoying; perfect examples of this anti-pattern realization might be found on any bank or air company website);
      • -
      • both questions and answers must be formulated clearly and succinctly; it's acceptable (and even desirable) to provide links to corresponding reference and tutorial articles, but the answer itself can't be longer than a couple of paragraphs.
      • +
      • Address the real questions (you might frequently find FAQs that were reflecting not users' needs, but the API owner's desire to repeat some important information once more; it's useless, or worse — annoying; perfect examples of this anti-pattern realization might be found on any bank or air company website)
      • +
      • Both questions and answers must be formulated clearly and succinctly. It's acceptable (and even desirable) to provide links to corresponding reference and tutorial articles, but the answer itself can't be longer than a couple of paragraphs.

      Also, FAQs are a convenient place to explicitly highlight the advantages of the API. In a question-answer form, you might demonstrably show how your API solves complex problems easily and handsomely. (Or at least, solves them, unlike the competitors' products.)

      If technical support conversations are public, it makes sense to store all the questions and answers as a separate service to form a knowledge base, i.e., a set of “real-life” questions and answers.

      @@ -5563,16 +5541,16 @@ Authorization: Bearer <token>

      Content Duplication Problems

      A significant problem that harms documentation clarity is API versioning: articles describing the same entity across different API versions are usually quite similar. Organizing convenient searching capability over such datasets is a problem for internal and external search engines as well. To tackle this problem ensure that:

        -
      • the API version is highlighted on the documentation pages;
      • -
      • if a version of the current page exists for newer API versions, there is an explicit link to the actual version;
      • -
      • docs for deprecated API versions are pessimized or even excluded from indexing.
      • +
      • The API version is highlighted on the documentation pages
      • +
      • If a version of the current page exists for newer API versions, there is an explicit link to the actual version
      • +
      • Docs for deprecated API versions are pessimized or even excluded from indexing.

      If you're strictly maintaining backward compatibility, it is possible to create a single documentation for all API versions. To do so, each entity is to be marked with the API version it is supported from. However, there is an apparent problem with this approach: it's not that simple to get docs for a specific (outdated) API version (and, generally speaking, to understand which capabilities this API version provides). (Though the offline documentation we mentioned earlier will help.)

      The problem becomes worse if you're supporting not only different API versions but also different environments / platforms / programming languages; for example, if your UI lib supports both iOS and Android. Then both documentation versions are equal, and it's impossible to pessimize one of them.

      In this case, you need to choose one of the following strategies:

        -
      • if the documentation topic content is totally identical for every platform, i.e., only the code syntax differs, you will need to develop generalized documentation: each article provides code samples (and maybe some additional notes) for every supported platform on a single page;
      • -
      • on the contrary, if the content differs significantly, as is in the iOS/Android case, we might suggest splitting the documentation sites (up to having separate domains for each platform): the good news is that developers almost always need one specific version, and they don't care about other platforms.
      • +
      • If the documentation topic content is totally identical for every platform, i.e., only the code syntax differs, you will need to develop generalized documentation: each article provides code samples (and maybe some additional notes) for every supported platform on a single page.
      • +
      • On the contrary, if the content differs significantly, as is in the iOS/Android case, we might suggest splitting the documentation sites (up to having separate domains for each platform): the good news is that developers almost always need one specific version, and they don't care about other platforms.

      The Documentation Quality

      The best documentation happens when you start viewing it as a product in the API product range, i.e., begin analyzing customer experience (with specialized tools), collect and process feedback, set KPIs and work on improving them.

      @@ -5582,9 +5560,9 @@ Authorization: Bearer <token>

      However, in many cases having a test version is not enough — like in our coffee-machine API example. If an order is created but not served, partners are not able to test the functionality of delivering the order or requesting a refund. To run the full cycle of testing, developers need the capability of pushing the order through stages, as this would happen in reality.

      A direct solution to this problem is providing test versions for a full set of APIs and administrative interfaces. It means that developers will be able to run a second application in parallel — the one you're giving to coffee shops so they might get and serve orders (and if there is a delivery functionality, the third app as well: the courier's one) — and make all these actions that coffee shop staff normally does. Obviously, that's not an ideal solution, because of several reasons:

        -
      • developers of end user applications will need to additionally learn how coffee shop and courier apps work, which has nothing to do with the task they're solving;
      • -
      • you will need to invent and implement some matching algorithm: an order made through a test application must be assigned to a specific virtual courier; this actually means creating an isolated virtual “sandbox” (meaning — a full set of services) for each specific partner;
      • -
      • executing a full “happy path” of an order will take minutes, maybe tens of minutes, and will require making a multitude of actions in several different interfaces.
      • +
      • Developers of end user applications will need to additionally learn how coffee shop and courier apps work, which has nothing to do with the task they're solving.
      • +
      • You will need to invent and implement some matching algorithm: an order made through a test application must be assigned to a specific virtual courier; this actually means creating an isolated virtual “sandbox” (meaning — a full set of services) for each specific partner.
      • +
      • Executing a full “happy path” of an order will take minutes, maybe tens of minutes, and will require making a multitude of actions in several different interfaces.

      There are two main approaches to tackling these problems.

      1. The Testing Environment API
      diff --git a/docs/API.en.pdf b/docs/API.en.pdf index f446ded..6bed6dc 100644 Binary files a/docs/API.en.pdf and b/docs/API.en.pdf differ diff --git a/docs/API.ru.epub b/docs/API.ru.epub index c709a47..f81d808 100644 Binary files a/docs/API.ru.epub and b/docs/API.ru.epub differ diff --git a/docs/API.ru.html b/docs/API.ru.html index bfbc0ce..d499a84 100644 --- a/docs/API.ru.html +++ b/docs/API.ru.html @@ -3873,17 +3873,17 @@ let status = api.getStatus(order.id);
      let order = api.createOrder();
       let status;
       while (true) {
      -    try {
      -        status = api.getStatus(order.id);
      -    } catch (e) {
      -        if (e.httpStatusCode != 404 || 
      -            timeoutExceeded()) {
      -            break;
      -        }
      +  try {
      +    status = api.getStatus(order.id);
      +  } catch (e) {
      +    if (e.httpStatusCode != 404 ||
      +      timeoutExceeded()) {
      +      break;
           }
      +  }
       }
       if (status) {
      -    …
      +  …
       }
       

      Мы полагаем, что можно не уточнять, что писать код, подобный вышеприведённому, ни в коем случае нельзя. Уж если вы действительно предоставляете нестрого консистентный API, то либо операция createOrder в SDK должна быть асинхронной и возвращать результат только по готовности всех реплик, либо политика перезапросов должна быть скрыта внутри операции getStatus.

      @@ -3955,7 +3955,7 @@ object.observe('widthchange', observerFunction); PUT /v1/api-types/{api_type} { "order_execution_endpoint": { - // Описание функции обратного вызова + // Callback function description } }
    @@ -3964,12 +3964,12 @@ PUT /v1/api-types/{api_type} PUT /v1/partners/{partnerId}/coffee-machines { "coffee_machines": [{ - "id", "api_type", "location", "supported_recipes" }, …] } +

    Таким образом механика следующая:

      @@ -4048,7 +4048,7 @@ POST /v1/recipes
      "product_properties": {
         // "l10n" — стандартное сокращение
         // для "localization"
      -  "l10n" : [{
      +  "l10n": [{
           "language_code": "en", 
           "country_code": "US", 
           "name", 
      @@ -5116,6 +5116,7 @@ Authorization: Bearer <token>
       
    • техническая поддержка внешних пользователей осуществляется по остаточному принципу.
    +
  • Разработчики внутренних сервисов часто ломают обратную совместимость или выпускают новые мажорные версии, совершенно не заботясь о последствиях этих действий для внешних партнёрах.
  • Всё это приводит к тому, что наличие внешнего API зачастую работает не в плюс компании, а в минус: фактически, вы предоставляете крайне критически и скептически настроенной аудитории очень плохой продукт. Если у вас нет ресурсов на грамотное развитие API как продукта для внешних пользователей — лучше за него не браться совсем.

    5. API = площадка для рекламы
    diff --git a/docs/API.ru.pdf b/docs/API.ru.pdf index 6cb64a9..9dbc632 100644 Binary files a/docs/API.ru.pdf and b/docs/API.ru.pdf differ diff --git a/src/en/clean-copy/04-Section III. The Backward Compatibility/04.md b/src/en/clean-copy/04-Section III. The Backward Compatibility/04.md index 23dfe0c..8ee1e0a 100644 --- a/src/en/clean-copy/04-Section III. The Backward Compatibility/04.md +++ b/src/en/clean-copy/04-Section III. The Backward Compatibility/04.md @@ -1,8 +1,8 @@ ### [Strong Coupling and Related Problems][back-compat-strong-coupling] -To demonstrate the strong coupling problematics let us move to *really interesting* things. Let's continue our “variation analysis”: what if the partners wish to offer not only the standard beverages but their own unique coffee recipes to end-users? The catch is that the partner API as we described it in the previous chapter does not expose the very existence of the partner network to the end user, and thus describes a simple case. Once we start providing methods to alter the core functionality, not just API extensions, we will soon face next-level problems. +To demonstrate the problems of strong coupling, let's move on to *interesting* topics. Let's continue our “variation analysis”: what if partners wish to offer their own unique coffee recipes to end users in addition to the standard beverages? The challenge is that the partner API, as described in the previous chapter, does not expose the very existence of the partner network to the end user, thus presenting a simple case. However, once we start providing methods to modify the core functionality, not just API extensions, we will soon face next-level problems. -So, let us add one more endpoint for registering the partner's own recipe: +So, let's add one more endpoint for registering the partner's own recipe: ``` // Adds new recipe @@ -14,21 +14,21 @@ POST /v1/recipes "description", "default_volume" // Other properties to describe - // the beverage to end-user + // the beverage to an end user … } } ``` -At first glance, again, it looks like a reasonably simple interface, explicitly decomposed into abstraction levels. But let us imagine the future — what would happen with this interface when our system evolves further? +At first glance, this appears to be a reasonably simple interface, explicitly decomposed into abstraction levels. But let's imagine the future and consider what would happen to this interface as our system evolves further. -The first problem is obvious to those who read the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter thoroughly: product properties must be localized. That will lead us to the first change: +The first problem is obvious to those who thoroughly read the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter: product properties must be localized. This leads us to the first change: ``` "product_properties": { // "l10n" is the standard abbreviation // for "localization" - "l10n" : [{ + "l10n": [{ "language_code": "en", "country_code": "US", "name", @@ -37,25 +37,25 @@ The first problem is obvious to those who read the “[Describing Final Interfac } ``` -And here the first big question arises: what should we do with the `default_volume` field? From one side, that's an objective property measured in standardized units, and it's being passed to the program execution engine. On the other side, in countries like the United States, we had to specify beverage volumes not like “300 ml,” but “10 fl oz.” We may propose two solutions: - * Either the partner provides the corresponding number only, and we will make readable descriptions on our own behalf, or - * The partner provides both the number and all of its localized representations. +And here arises the first big question: what should we do with the `default_volume` field? On one hand, it's an objective property measured in standardized units to be passed to the program execution engine. On the other hand, in countries like the United States, beverage volumes are specified as “10 fl oz” rather than “300 ml.” We can propose two solutions: + * Either the partner provides only the corresponding number and we will make readable descriptions ourselves, or + * The partner provides both the number and all its localized representations. -The flaw in the first option is that a partner might be willing to use the service in some new country or language — and will be unable to do so until the API supports them. The flaw in the second option is that it works with predefined volumes only, so you can't order an arbitrary beverage volume. So the very first step we've made effectively has us trapped. +The flaw in the first option is that a partner might be willing to use the service in a new country or language, but they will be unable to do so until the API is localized to support these new territories. The flaw in the second option is that it only works with predefined volumes, so ordering an arbitrary beverage volume will not be possible. The very first step we've taken effectively has had us trapped. -The localization flaws are not the only problem with this API. We should ask ourselves a question — *why* do we really need these `name` and `description`? They are simply non-machine-readable strings with no specific semantics. At first glance, we need them to return them back in the `/v1/search` method response, but that's not a proper answer: why do we really return these strings from `search`? +The localization flaws are not the only problem with this API. We should ask ourselves a question: *why* do we really need these `name` and `description` fields? They are simply non-machine-readable strings with no specific semantics. At first glance, we need them to return in the `/v1/search` method response, but that's not a proper answer as it only leads to another question: why do we actually return these strings from `search`? -The correct answer lies a way beyond this specific interface. We need them *because some representation exists*. There is a UI for choosing beverage type. Probably the `name` and `description` fields are simply two designations of the beverage for a user to read, a short one (to be displayed on the search results page) and a long one (to be displayed in the extended product specification block). It actually means that we set the requirements to the API based on some specific design. But *what if* a partner is making their own UI for their own app? Not only they might not actually need two descriptions, but we are also *deceiving* them. The `name` is not “just a name”, it implies some restrictions: it has recommended length which is optimal to some specific UI, and it must look consistently on the search results page. Indeed, the “our best quality™ coffee” or “Invigorating Morning Freshness®” designations would look very weird in-between “Cappuccino,” “Lungo,” and “Latte.” +The correct answer lies beyond this specific interface. We need them *because some representation exists*. There is a UI for choosing a beverage type. The `name` and `description` fields are probably two designations of the beverage for the user to read, a short one (to be displayed on the search results page) and a long one (to be displayed in the extended product specification block). This means we are setting the API requirements based on some specific visual design. But *what if* a partner is creating their own UI for their own app? Not only might they not actually need two descriptions, but we are also *deceiving* them. The `name` is not “just a name” as it implies certain restrictions: it has a recommended length that is optimal for a specific UI, and it must look consistent on the search results page. Indeed, designations like “our best quality™ coffee” or “Invigorating Morning Freshness®” would look out of place among “Cappuccino,” “Lungo,” and “Latte.” -There is also another side to this story. As UIs (both ours' and partners') tend to evolve, new visual elements will be eventually introduced. For example, a picture of the beverage, its energy value, allergen information, etc. The `product_properties` entity will become a scrapyard for tons of optional fields, and learning how setting what field results in what effects in the UI will be an interesting quest, full of probes and mistakes. +There is also another aspect to consider. As UIs (both ours and partners') evolve, new visual elements will eventually be introduced. For example, a picture of the beverage, its energy value, allergen information, etc. The `product_properties` entity will become a scrapyard for numerous optional fields, and learning how to set each field and its effects in the UI will be an interesting journey filled with trial and error. -The problems we're facing are the problems of *strong coupling*. Each time we offer an interface like described above, we in fact prescript implementing one entity (recipe) based on implementations of other entities (UI layout, localization rules). This approach disrespects the very basic principle of the “top to bottom” API design because **low-level entities must not define high-level ones**. +The problems we're facing are the problems of *strong coupling*. Each time we offer an interface as described above, we effectively dictate the implementation of one entity (recipe) based on the implementations of other entities (UI layout, localization rules). This approach disregards the fundamental principle of “top to bottom” API design because **low-level entities should not define high-level ones**. #### The Rule of Contexts -To make things worse, let us state that the inverse principle is also correct: high-level entities must not define low-level ones as well, 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 the interfaces for adding a new recipe 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. +To exacerbate matters, let us state that the inverse principle is also true: high-level entities should not define low-level ones as well since it is not their responsibility. The way out of this logical labyrinth is that high-level entities should *define a context* for other objects to interpret. To properly design the interfaces for adding a new recipe we should not attempt to find a better data format. Instead, we need to understand the explicit and implicit contexts that exist in our subject area. -We have already noted a localization context. There is some set of languages and regions we support in our API, and there are the requirements — what exactly partners 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, either internally or within an SDK: +We have already identified a localization context. There is a set of languages and regions supported by our API, and there are requirements for what partners must provide to make the API work in a new region. Specifically, there must be a formatting function to represent beverage volume somewhere in our API code, either internally or within an SDK: ``` l10n.volume.format = function( @@ -71,7 +71,7 @@ l10n.volume.format = function( */ ``` -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 through the partner API. Like this: +To ensure our API works correctly with a new language or region, the partner must either define this function or indicate which pre-existing implementation to use through the partner API, like this: ``` // Add a general formatting rule @@ -85,8 +85,8 @@ PUT /formatters/volume/ru // in the “US” region PUT /formatters/volume/ru/US { - // in the US, we need to recalculate - // the number, then add a postfix + // In the US, we need to recalculate + // the number and add a postfix "value_transform": { "action": "divide", "divisor": 30 @@ -95,20 +95,19 @@ PUT /formatters/volume/ru/US } ``` -so the above-mentioned `l10n.volume.format` function implementation might retrieve the formatting rules for the new language-region pair and use them. +so the aforementioned `l10n.volume.format` function implementation can retrieve the formatting rules for the new language-region pair and utilize them. -**NB**: we are more than aware that such a simple format isn't enough to cover real-world localization use cases, and one either relies on existing libraries or designs a sophisticated format for such templating, which takes into account such things as grammatical cases and rules of rounding numbers up or allow defining formatting rules in a form of function code. The example above is simplified for purely educational purposes. - -Let us deal with the `name` and `description` problem then. To lower the coupling level there, we need to formalize (probably just to ourselves) a “layout” concept. We are asking for providing the `name` and `description` fields not because we just need them, but for representing them in some specific user interface. This specific UI might have an identifier or a semantic name. +**NB**: we are well aware that such a simple format is not sufficient to cover real-world localization use cases, and one would either rely on existing libraries or design a sophisticated format for such templating, which takes into account various aspects such as grammatical cases and rules for rounding numbers or allows defining formatting rules in the form of function code. The example above is simplified for purely educational purposes. +Let's address the `name` and `description` problem. To reduce the coupling level, we need to formalize (probably just for ourselves) a “layout” concept. We request the provision of the `name` and `description` fields not because we theoretically need them but to present them in a specific user interface. This particular UI might have an identifier or a semantic name associated with it: ``` GET /v1/layouts/{layout_id} { "id", - // We would probably have lots of layouts, - // so it's better to enable extensibility - // from the beginning + // Since we will likely have numerous + // layouts, it's better to enable + // extensibility from the beginning "kind": "recipe_search", // Describe every property we require // to have this layout rendered properly @@ -116,11 +115,11 @@ GET /v1/layouts/{layout_id} // Since we learned that `name` // is actually a title for a search // result snippet, it's much more - // convenient to have explicit + // convenient to have an explicit // `search_title` instead "field": "search_title", "view": { - // Machine-readable description + // A machine-readable description // of how this field is rendered "min_length": "5em", "max_length": "20em", @@ -135,7 +134,7 @@ GET /v1/layouts/{layout_id} } ``` -So the partner may decide, which option better suits them. They can provide mandatory fields for the standard layout: +Thus, the partner can decide which option better suits their needs. They can provide mandatory fields for the standard layout: ``` PUT /v1/recipes/{id}/properties/l10n/{lang} @@ -144,9 +143,9 @@ PUT /v1/recipes/{id}/properties/l10n/{lang} } ``` -or create a layout of their own and provide the data fields it requires, or they may ultimately design their own UI and don't use this functionality at all, defining neither layouts nor corresponding data fields. +Alternatively, they can create their own layout and provide the data fields it requires, or they may choose to design their own UI and not use this functionality at all, thereby defining neither layouts nor corresponding data fields. -Then our interface would ultimately look like this: +Ultimately, our interface would look like this: ``` POST /v1/recipes @@ -155,7 +154,7 @@ POST /v1/recipes { "id" } ``` -This conclusion might look highly counter-intuitive, but lacking any fields in a `Recipe` simply tells us that this entity possesses no specific semantics of its own, and is simply an identifier of a context; a method to point out where to look for the data needed by other entities. In the real world, we should implement a builder endpoint capable of creating all the related contexts with a single request: +This conclusion might seem highly counter-intuitive, but the absence of fields in a `Recipe` simply tells us that this entity possesses no specific semantics of its own. It serves solely as an identifier of a context, a way to indicate where to find the data needed by other entities. In the real world, we should implement a builder endpoint capable of creating all the related contexts with a single request: ``` POST /v1/recipe-builder @@ -166,11 +165,11 @@ POST /v1/recipe-builder "default_volume", "l10n" }, - // Create all the desirable layouts + // Create all the desired layouts "layouts": [{ "id", "kind", "properties" }], - // Add all the formatters needed + // Add all the required formatters "formatters": { "volume": [ { @@ -183,13 +182,14 @@ POST /v1/recipe-builder } ] }, - // Other actions needed to be done - // to register new recipe in the system + // Other actions needed + // to register a new recipe + // in the system … } ``` -We should also note that providing a newly created entity identifier by the requesting side isn't exactly the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, we're risking getting lots of collisions on recipe names used by different partners, so we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter. +We should also note that providing a newly created entity identifier from the requesting side is not the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, there is a risk of encountering collisions with recipe names used by different partners. Therefore, we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter. ``` POST /v1/recipes/custom @@ -210,19 +210,19 @@ POST /v1/recipes/custom Also note that this format allows us to maintain an important extensibility point: different partners might have both shared and isolated namespaces. Furthermore, we might introduce special namespaces (like `common`, for example) to allow editing standard recipes (and thus organizing our own recipes backoffice). -**NB**: a mindful reader might have noted that this technique was already used in our API study much earlier in the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter with regards to the “program” and “program run” entities. Indeed, we might do it without the `program-matcher` endpoint and make it this way: +**NB**: a mindful reader might have noticed that this technique was already used in our API study much earlier in the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter regarding the “program” and “program run” entities. Indeed, we can propose an interface for retrieving commands to execute a specific recipe without the `program-matcher` endpoint, and instead, do it this way: ``` GET /v1/recipes/{id}/run-data/{api_type} → -{ /* A description, how to +{ /* A description of how to execute a specific recipe using a specified API type */ } ``` -Then developers would have to make this trick to get coffee prepared: +Then developers would have to make this trick to get the beverage prepared: * Learn the API type of the specific coffee machine. - * Get the execution description, as stated above. - * Depending on the API type, run some specific commands. + * Retrieve the execution description as described above. + * Based on the API type, execute specific commands. -Obviously, such an interface is absolutely unacceptable, simply because in the majority of use cases developers don't care at all, which API type the specific coffee machine runs. To avoid the necessity of introducing such bad interfaces we created a new “program” entity, which constitutes merely a context identifier, just like a “recipe” entity does. A `program_run_id` entity is also organized in this manner, it also possesses no specific properties, being *just* a program run identifier. \ No newline at end of file +Obviously, such an interface is completely unacceptable because, in the majority of use cases, developers do not care at all about which API type the specific coffee machine exposes. To avoid the need for introducing such poor interfaces we created a new “program” entity, which serves solely as a context identifier, just like a “recipe” entity does. Similarly, the `program_run_id` entity is also organized in the same manner, without possessing any specific properties and representing *just* a program run identifier. \ No newline at end of file diff --git a/src/ru/clean-copy/04-Раздел III. Обратная совместимость/04.md b/src/ru/clean-copy/04-Раздел III. Обратная совместимость/04.md index f5f2967..bca01c4 100644 --- a/src/ru/clean-copy/04-Раздел III. Обратная совместимость/04.md +++ b/src/ru/clean-copy/04-Раздел III. Обратная совместимость/04.md @@ -28,7 +28,7 @@ POST /v1/recipes "product_properties": { // "l10n" — стандартное сокращение // для "localization" - "l10n" : [{ + "l10n": [{ "language_code": "en", "country_code": "US", "name",