1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-05-31 22:09:37 +02:00

typography

This commit is contained in:
Sergey Konstantinov 2023-02-28 17:37:27 +02:00 committed by GitHub
parent 5a5e5ac1c4
commit a3ca1d1f1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 193 additions and 193 deletions

View File

@ -1,6 +1,6 @@
### On the Structure of This Book
The book you're holding in your hands comprises this Introduction and three sections: ‘The API Design,’ ‘The Backwards Compatibility,’ and ‘The API Product.’
The book you're holding in your hands comprises this Introduction and three sections: “The API Design,” “The Backwards Compatibility,” and “The API Product.”
In Section I, we will discuss designing APIs as a concept: how to build the architecture properly, from high-level planning down to final interfaces.

View File

@ -1,6 +1,6 @@
### The API Definition
Before we start talking about the API design, we need to explicitly define what the API is. Encyclopedia tells us that ‘API’ is an acronym for ‘Application Program Interface’. This definition is fine, but useless. Much like ‘Man’ definition by Plato: Man stood upright on two legs without feathers. This definition is fine again, but it gives us no understanding of what's so important about a Man. (Actually, not ‘fine’ either. Diogenes of Sinope once brought a plucked chicken, saying ‘That's Plato's Man’. And Plato had to add ‘with broad nails’ to his definition.)
Before we start talking about the API design, we need to explicitly define what the API is. Encyclopedia tells us that “API” is an acronym for “Application Program Interface.” This definition is fine, but useless. Much like “Man” definition by Plato: Man stood upright on two legs without feathers. This definition is fine again, but it gives us no understanding of what's so important about a Man. (Actually, not “fine” either. Diogenes of Sinope once brought a plucked chicken, saying “That's Plato's Man.” And Plato had to add “with broad nails” to his definition.)
What API *means* apart from the formal definition?

View File

@ -1,16 +1,16 @@
### API Quality Criteria
Before we start laying out the recommendations, we ought to specify what API we consider ‘fine’, and what's the profit of having a ‘fine’ API.
Before we start laying out the recommendations, we ought to specify what API we consider “fine,” and what's the profit of having a “fine” API.
Let's discuss the second question first. Obviously, API ‘finesse’ is first of all defined through its capability to solve developers' problems. (One may reasonably say that solving developers' problems might not be the main purpose of offering the API of ours to developers. However, manipulating public opinion is out of this book's author's interest. Here we assume that APIs exist primarily to help developers in solving their problems, not for some other covertly declared purposes.)
Let's discuss the second question first. Obviously, API “finesse” is first of all defined through its capability to solve developers' problems. (One may reasonably say that solving developers' problems might not be the main purpose of offering the API of ours to developers. However, manipulating public opinion is out of this book's author's interest. Here we assume that APIs exist primarily to help developers in solving their problems, not for some other covertly declared purposes.)
So, how the API design might help the developers? Quite simple: a well-designed API must solve their problems in the most efficient and comprehensible manner. The distance from formulating the task to writing a working code must be as short as possible. Among other things, it means that:
* it must be totally obvious out of your API's structure how to solve a task; ideally, developers at first glance should be able to understand, what entities are meant to solve their problem;
* the API must be readable; ideally, developers write correct code after just looking at the method nomenclature, never bothering about details (especially API implementation details!); it is also very important to mention, that not only problem solution (the ‘happy path’) should be obvious, but also possible errors and exceptions (the ‘unhappy path’);
* the API must be readable; ideally, developers write correct code after just looking at the method nomenclature, never bothering about details (especially API implementation details!); it is also very important to mention, that not only problem solution (the “happy path”) should be obvious, but also possible errors and exceptions (the “unhappy path”);
* the API must be consistent; while developing new functionality (i.e. while using unknown API entities) developers may write new code similar to the code they already wrote using known API concepts, and this new code will work.
However static convenience and clarity of APIs are simple parts. After all, nobody seeks for making an API deliberately irrational and unreadable. When we are developing an API, we always start with clear basic concepts. Providing you've got some experience in APIs, it's quite hard to make an API core that fails to meet obviousness, readability, and consistency criteria.
Problems begin when we start to expand our API. Adding new functionality sooner or later results in transforming once plain and simple API into a mess of conflicting concepts, and our efforts to maintain backwards compatibility lead to illogical, unobvious, and simply bad design solutions. It is partly related to an inability to predict the future in detail: your understanding of ‘fine’ APIs will change over time, both in objective terms (what problems the API is to solve, and what are the best practices) and in subjective ones too (what obviousness, readability, and consistency *really mean* to your API design).
Problems begin when we start to expand our API. Adding new functionality sooner or later results in transforming once plain and simple API into a mess of conflicting concepts, and our efforts to maintain backwards compatibility lead to illogical, unobvious, and simply bad design solutions. It is partly related to an inability to predict the future in detail: your understanding of “fine” APIs will change over time, both in objective terms (what problems the API is to solve, and what are the best practices) and in subjective ones too (what obviousness, readability, and consistency *really mean* to your API design).
The principles we are explaining below are specifically oriented to making APIs evolve smoothly over time, not being turned into a pile of mixed inconsistent interfaces. It is crucial to understand that this approach isn't free: a necessity to bear in mind all possible extension variants and to preserve essential growth points means interface redundancy and possibly excessing abstractions being embedded in the API design. Besides both make the developers' jobs harder. **Providing excess design complexities being reserved for future use makes sense only if this future actually exists for your API. Otherwise, it's simply an overengineering.**

View File

@ -2,7 +2,7 @@
Backwards compatibility is a *temporal* characteristic of your API. An obligation to maintain backwards compatibility is the crucial point where API development differs from software development in general.
Of course, backwards compatibility isn't an absolute. In some subject areas shipping new backwards-incompatible API versions is a routine. Nevertheless, every time you deploy a new backwards-incompatible API version, the developers need to make some non-zero effort to adapt their code to the new API version. In this sense, releasing new API versions puts a sort of a ‘tax’ on customers. They must spend quite real money just to make sure their product continues working.
Of course, backwards compatibility isn't an absolute. In some subject areas shipping new backwards-incompatible API versions is a routine. Nevertheless, every time you deploy a new backwards-incompatible API version, the developers need to make some non-zero effort to adapt their code to the new API version. In this sense, releasing new API versions puts a sort of a “tax” on customers. They must spend quite real money just to make sure their product continues working.
Large companies, which occupy firm market positions, could afford to imply such taxation. Furthermore, they may introduce penalties for those who refuse to adapt their code to new API versions, up to disabling their applications.

View File

@ -7,8 +7,8 @@ Here and throughout we firmly stick to [semver](https://semver.org/) principles
2. The second number (a minor version) increases when new functionality is added to the API, keeping backwards compatibility intact.
3. The third number (a patch) increases when a new API version contains bug fixes only.
Sentences ‘major API version’ and ‘new API version, containing backwards-incompatible changes’ are therefore to be considered equivalent ones.
Sentences “major API version” and “new API version, containing backwards-incompatible changes” are therefore to be considered equivalent ones.
It is usually (though not necessary) agreed that the last stable API version might be referenced by either a full version (e.g. `1.2.3`) or a reduced one (`1.2` or just `1`). Some systems support more sophisticated schemes of defining the desired version (for example, `^1.2.3` reads like ‘get the last stable API version that is backwards-compatible to the `1.2.3` version’) or additional shortcuts (for example, `1.2-beta` to refer to the last beta-version of the `1.2` API version family). In this book, we will mostly use designations like `v1` (`v2`, `v3`, etc.) to denote the latest stable release of the `1.x.x` version family of an API.
It is usually (though not necessary) agreed that the last stable API version might be referenced by either a full version (e.g. `1.2.3`) or a reduced one (`1.2` or just `1`). Some systems support more sophisticated schemes of defining the desired version (for example, `^1.2.3` reads like “get the last stable API version that is backwards-compatible to the `1.2.3` version”) or additional shortcuts (for example, `1.2-beta` to refer to the last beta-version of the `1.2` API version family). In this book, we will mostly use designations like `v1` (`v2`, `v3`, etc.) to denote the latest stable release of the `1.x.x` version family of an API.
The practical meaning of this versioning system and the applicable policies will be discussed in more detail in the ‘Backwards Compatibility Problem Statement’ chapter.
The practical meaning of this versioning system and the applicable policies will be discussed in more detail in the “Backwards Compatibility Problem Statement” chapter.

View File

@ -1,8 +1,8 @@
### Terms and Notation Keys
Software development is being characterized, among other things, by the existence of many different engineering paradigms, whose adepts sometimes are quite aggressive towards other paradigms' adepts. While writing this book we are deliberately avoiding using terms like ‘method’, ‘object’, ‘function’, and so on, using the neutral term ‘entity’ instead. ‘Entity’ means some atomic functionality unit, like class, method, object, monad, prototype (underline what you think is right).
Software development is being characterized, among other things, by the existence of many different engineering paradigms, whose adepts sometimes are quite aggressive towards other paradigms' adepts. While writing this book we are deliberately avoiding using terms like “method”, “object”, “function”, and so on, using the neutral term “entity” instead. “Entity” means some atomic functionality unit, like class, method, object, monad, prototype (underline what you think is right).
As for an entity's components, we regretfully failed to find a proper term, so we will use the words ‘fields’ and ‘methods’.
As for an entity's components, we regretfully failed to find a proper term, so we will use the words “fields” and “methods.”
Most of the examples of APIs will be provided in a form of JSON-over-HTTP endpoints. This is some sort of notation that, as we see it, helps to describe concepts in the most comprehensible manner. A `GET /v1/orders` endpoint call could easily be replaced with an `orders.get()` method call, local or remote; JSON could easily be replaced with any other data format. The semantics of statements shouldn't change.
@ -41,12 +41,12 @@ It should be read like this:
* the response body is a JSON comprising a single `error_message` field; field value absence means that field contains exactly what you expect it should contain — some error message in this case;
* if some token is too long to fit a single line, we will split it into several lines adding `⮠` to indicate it continues next line.
The term ‘client’ here stands for an application being executed on a user's device, either a native or a web one. The terms ‘agent’ and ‘user agent’ are synonymous to ‘client’.
The term “client” here stands for an application being executed on a user's device, either a native or a web one. The terms “agent” and “user agent” are synonymous to “client.”
Some request and response parts might be omitted if they are irrelevant to the topic being discussed.
Simplified notation might be used to avoid redundancies, like `POST /some-resource` `{…, "some_parameter", …}``{ "operation_id" }`; request and response bodies might also be omitted.
We will be using sentences like `POST /v1/bucket/{id}/some-resource` method’ (or simply ‘`bucket/some-resource` method’, ‘`some-resource` method — if there are no other `some-resource`s in the chapter, so there is no ambiguity) to refer to such endpoint definitions.
We will be using sentences like `POST /v1/bucket/{id}/some-resource` method” (or simply “`bucket/some-resource` method,” “`some-resource` method — if there are no other `some-resource`s in the chapter, so there is no ambiguity) to refer to such endpoint definitions.
Apart from HTTP API notation, we will employ C-style pseudocode, or, to be more precise, JavaScript-like or Python-like since types are omitted. We assume such imperative structures are readable enough to skip detailed grammar explanations.

View File

@ -12,13 +12,13 @@ The key question you should ask yourself looks like that: what problem do we sol
So, let's imagine that we are going to develop an API for automated coffee ordering in city cafes, and let's apply the key question to it.
1. Why would someone need an API to make a coffee? Why ordering a coffee via ‘human-to-human’ or ‘human-to-machine’ interfaces is inconvenient, why have a ‘machine-to-machine’ interface?
1. Why would someone need an API to make a coffee? Why ordering a coffee via “human-to-human” or “human-to-machine” interfaces is inconvenient, why have a “machine-to-machine” interface?
* Possibly, we're solving awareness and selection problems? To provide humans with full knowledge of what options they have right now and right here.
* Possibly, we're optimizing waiting times? To save the time people waste while waiting for their beverages.
* Possibly, we're reducing the number of errors? To help people get exactly what they wanted to order, stop losing information in imprecise conversational communication, or in dealing with unfamiliar coffee machine interfaces?
The ‘why’ question is the most important of all questions you must ask yourself. And not only about global project goals, but also locally about every single piece of functionality. **If you can't briefly and clearly answer the question ‘what this entity is needed for’ then it's not needed**.
The “why” question is the most important of all questions you must ask yourself. And not only about global project goals, but also locally about every single piece of functionality. **If you can't briefly and clearly answer the question “what this entity is needed for” then it's not needed**.
Here and throughout we assume, to make our example more complex and bizarre, that we are optimizing all three factors.
@ -38,7 +38,7 @@ In other words, there must be a solid reason to split two software development d
We should also note that you should try making an API when, and only when, your answer is "because that's our area of expertise" to question 3. Developing APIs is a sort of meta-engineering: you're writing some software to allow other companies to develop software to solve users' problems. You must possess expertise in both domains (APIs and user products) to design your API well.
As for our speculative example, let us imagine that in the near future some tectonic shift happened within the coffee brewing market. Two distinct player groups took shape: some companies provide ‘hardware’, i.e. coffee machines; other companies have access to customer auditory. Something like the flights market looks like: there are air companies, which actually transport passengers; and there are trip planning services where users are choosing between trip variants the system generates for them. We're aggregating hardware access to allow app vendors for ordering freshly brewed coffee.
As for our speculative example, let us imagine that in the near future some tectonic shift happened within the coffee brewing market. Two distinct player groups took shape: some companies provide “hardware,” i.e. coffee machines; other companies have access to customer auditory. Something like the flights market looks like: there are air companies, which actually transport passengers; and there are trip planning services where users are choosing between trip variants the system generates for them. We're aggregating hardware access to allow app vendors for ordering freshly brewed coffee.
#### What and How
@ -48,4 +48,4 @@ After finishing all these theoretical exercises, we should proceed right to desi
In our coffee case, we are:
* providing an API to services with a larger audience, so their users may order a cup of coffee in the most efficient and convenient manner;
* abstracting access to coffee machines ‘hardware’ and delivering methods to select a beverage kind and some location to brew — and to make an order.
* abstracting access to coffee machines “hardware” and delivering methods to select a beverage kind and some location to brew — and to make an order.

View File

@ -1,6 +1,6 @@
### Separating Abstraction Levels
‘Separate abstraction levels in your code’ is possibly the most general advice to software developers. However, we don't think it would be a grave exaggeration to say that abstraction levels separation is also the most difficult task for API developers.
“Separate abstraction levels in your code” is possibly the most general advice to software developers. However, we don't think it would be a grave exaggeration to say that abstraction levels separation is also the most difficult task for API developers.
Before proceeding to the theory, we should formulate clearly *why* abstraction levels are so important, and what goals we're trying to achieve by separating them.
@ -12,7 +12,7 @@ Back to our coffee example. What entity abstraction levels do we see?
2. Each cup of coffee is prepared according to some `recipe`, which implies the presence of different ingredients and sequences of preparation steps.
3. Each beverage is being prepared on some physical `coffee machine`, occupying some position in space.
Every level presents a developer-facing ‘facet’ in our API. While elaborating on the hierarchy of abstractions, we are first of all trying to reduce the interconnectivity of different entities. That would help us to reach several goals.
Every level presents a developer-facing “facet” in our API. While elaborating on the hierarchy of abstractions, we are first of all trying to reduce the interconnectivity of different entities. That would help us to reach several goals.
1. Simplifying developers' work and the learning curve. At each moment of time, a developer is operating only those entities which are necessary for the task they're solving right now. And conversely, badly designed isolation leads to the situation when developers have to keep in mind lots of concepts mostly unrelated to the task being solved.
@ -49,11 +49,11 @@ Then a developer just needs to compare two numbers to find out whether the order
This solution intuitively looks bad, and it really is: it violates all the abovementioned principles.
**First**, to solve the task ‘order a lungo’ a developer needs to refer to the ‘recipe’ entity and learn that every recipe has an associated volume. Then they need to embrace the concept that an order is ready at that particular moment when the prepared beverage volume becomes equal to the reference one. This concept is simply unguessable, and knowing it is mostly useless.
**First**, to solve the task “order a lungo” a developer needs to refer to the “recipe” entity and learn that every recipe has an associated volume. Then they need to embrace the concept that an order is ready at that particular moment when the prepared beverage volume becomes equal to the reference one. This concept is simply unguessable, and knowing it is mostly useless.
**Second**, we will have automatically got problems if we need to vary the beverage size. For example, if one day we decide to offer a choice to a customer, how many milliliters of lungo they desire exactly, then we have to perform one of the following tricks.
Variant I: we have a list of possible volumes fixed and introduce bogus recipes like `/recipes/small-lungo` or `recipes/large-lungo`. Why ‘bogus’? Because it's still the same lungo recipe, same ingredients, same preparation steps, only volumes differ. We will have to start the mass production of recipes, only different in volume, or introduce some recipe ‘inheritance’ to be able to specify the ‘base’ recipe and just redefine the volume.
Variant I: we have a list of possible volumes fixed and introduce bogus recipes like `/recipes/small-lungo` or `recipes/large-lungo`. Why “bogus”? Because it's still the same lungo recipe, same ingredients, same preparation steps, only volumes differ. We will have to start the mass production of recipes, only different in volume, or introduce some recipe “inheritance” to be able to specify the “base” recipe and just redefine the volume.
Variant II: we modify an interface, pronouncing volumes stated in recipes being just the default values. We allow requesting different cup volumes while placing an order:
@ -68,9 +68,9 @@ POST /v1/orders
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:
* there is a significant chance that developers will make mistakes in this functionality implementation if they add arbitrary volume support in the code working with the `POST /v1/orders` handler, but forget to make corresponding changes in the order readiness check code;
* the same field (coffee volume) now means different things in different interfaces. In the `GET /v1/recipes` context the `volume` field means a volume to be prepared if no arbitrary volume is specified in the `POST /v1/orders` request’; and it cannot be renamed to ‘default volume’ easily, we now have to live with that.
* the same field (coffee volume) now means different things in different interfaces. In the `GET /v1/recipes` context the `volume` field means a volume to be prepared if no arbitrary volume is specified in the `POST /v1/orders` request”; and it cannot be renamed to “default volume” easily, we now have to live with that.
**Third**, the entire scheme becomes totally inoperable if different types of coffee machines produce different volumes of lungo. To introduce the ‘lungo volume depends on machine type’ constraint we have to do quite a nasty thing: make recipes depend on coffee machine ids. By doing so we start actively ‘stir’ abstraction levels: one part of our API (recipe endpoints) becomes unusable without explicit knowledge of another part (coffee machines listing). And what is even worse, developers will have to change the logic of their apps: previously it was possible to choose volume first, then a coffee machine; but now this step must be rebuilt from scratch.
**Third**, the entire scheme becomes totally inoperable if different types of coffee machines produce different volumes of lungo. To introduce the “lungo volume depends on machine type” constraint we have to do quite a nasty thing: make recipes depend on coffee machine ids. By doing so we start actively “stir” abstraction levels: one part of our API (recipe endpoints) becomes unusable without explicit knowledge of another part (coffee machines listing). And what is even worse, developers will have to change the logic of their apps: previously it was possible to choose volume first, then a coffee machine; but now this step must be rebuilt from scratch.
Okay, we understood how to make things naughty. But how to make them *nice*?
@ -78,17 +78,17 @@ Abstraction levels separation should go in three directions:
1. From user scenarios to their internal representation: high-level entities and their method nomenclature must directly reflect API usage scenarios; low-level entities reflect the decomposition of scenarios into smaller parts.
2. From user subject field terms to ‘raw’ data subject field terms — in our case from high-level terms like ‘order’, ‘recipe’, ‘café’ to low-level terms like ‘beverage temperature’, ‘coffee machine geographical coordinates’, etc.
2. From user subject field terms to “raw” data subject field terms — in our case from high-level terms like “order”, “recipe”, “café” to low-level terms like “beverage temperature”, “coffee machine geographical coordinates”, etc.
3. Finally, from data structures suitable for end users to ‘raw’ data structures — in our case, from ‘lungo recipe’ and ‘"Chamomile" café chain’ to the raw byte data stream from ‘Good Morning’ coffee machine sensors.
3. Finally, from data structures suitable for end users to “raw” data structures — in our case, from “lungo recipe” and “"Chamomile" café chain” to the raw byte data stream from “Good Morning” coffee machine sensors.
The more is the distance between programmable contexts our API connects, the deeper is 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:
* from one side, an ‘order’ should not store the data regarding coffee machine sensors;
* from one side, an “order” should not store the data regarding coffee machine sensors;
* on the other side, a coffee machine should not store the data regarding order properties (and its API probably doesn't provide such functionality).
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 to another. For example, introduce a `task` entity like that:
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 to another. For example, introduce a `task` entity like that:
```
{
@ -109,13 +109,13 @@ A naïve approach to this situation is to design an interim abstraction level as
}
```
We call this approach ‘naïve’ not because it's wrong; on the contrary, that's quite a logical ‘default’ solution if you don't know yet (or don't understand yet) how your API will look like. The problem with this approach lies in its speculativeness: it doesn't reflect the subject area's organization.
We call this approach “naïve” not because it's wrong; on the contrary, that's quite a logical “default” solution if you don't know yet (or don't understand yet) how your API will look like. The problem with this approach lies in its speculativeness: it doesn't reflect the subject area's organization.
An experienced developer in this case must ask: what options do exist? How we really should determine beverage readiness? 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 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 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.
@ -223,29 +223,29 @@ To be more specific, let's assume those two kinds of coffee machines provide the
**NB**. The example is intentionally factitious to model a situation described above: to determine beverage readiness you have to compare the requested volume with volume sensor readings.
Now the picture becomes more apparent: we need to abstract coffee machine API calls so that the ‘execution level’ in our API provides general functions (like beverage readiness detection) in a unified form. We should also note that these two coffee machine API kinds belong to different abstraction levels themselves: the first one provides a higher-level API than the second one. Therefore, a ‘branch’ of our API working with second-kind machines will be more intricate.
Now the picture becomes more apparent: we need to abstract coffee machine API calls so that the “execution level” in our API provides general functions (like beverage readiness detection) in a unified form. We should also note that these two coffee machine API kinds belong to different abstraction levels themselves: the first one provides a higher-level API than the second one. Therefore, a “branch” of our API working with second-kind machines will be more intricate.
The next step in abstraction level separating is determining what functionality we're abstracting. To do so we need to understand the tasks developers solve at the ‘order’ level, and to learn what problems they get if our interim level is missing.
The next step in abstraction level separating is determining what functionality we're abstracting. To do so we need to understand the tasks developers solve at the “order” level, and to learn what problems they get if our interim level is missing.
1. Obviously, the developers desire to create an order uniformly: list high-level order properties (beverage kind, volume, and special options like syrup or milk type), and don't think about how the specific coffee machine executes it.
2. Developers must be able to learn the execution state: is the order ready? if not — when to expect it's ready (and is there any sense to wait in case of execution errors).
3. Developers need to address the order's location in space and time — to explain to users where and when they should pick the order up.
4. Finally, developers need to run atomic operations, like canceling orders.
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 explicit ‘ready’ status.
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 explicit “ready” status.
But with the second-kind API, it's much worse. The main problem we foresee is an absence of ‘memory’ for actions being executed. Functions and sensors API is totally stateless, which means we don't even understand who called a function being currently executed, when, and which order it relates.
But with the second-kind API, it's much worse. The main problem we foresee is an absence of “memory” for actions being executed. Functions and sensors API is totally stateless, which means we don't even understand who called a function being currently executed, when, and which order it relates.
So we need to introduce two abstraction levels.
1. Execution control level, which provides the uniform interface to indivisible programs. ‘Uniform interface’ means here that, regardless of a coffee machine's kind, developers may expect:
1. Execution control level, which provides the 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 error) being the same;
* methods nomenclature (for example, order cancellation method) and their behavior being the same.
2. Program runtime level. For the first-kind API, it will provide just a wrapper for existing programs API; for the second-kind API, the entire ‘runtime’ concept is to be developed from scratch by us.
2. Program runtime level. For the first-kind API, it will provide just a wrapper for existing programs API; for the second-kind API, the entire “runtime” concept is to be developed from scratch by us.
What does this mean in a practical sense? Developers will still be creating orders, dealing with high-level entities only:
@ -292,7 +292,7 @@ This approach has some benefits, like the possibility to provide different sets
* call `POST /execute` physical API method, passing internal program identifier — for the first API kind;
* initiate runtime creation to proceed with the second API kind.
Out of general concerns runtime level for the second-kind API will be private, so we are more or less free in implementing it. The easiest solution would be to develop a virtual state machine that creates a ‘runtime’ (e.g. a stateful execution context) to run a program and control its state.
Out of general concerns runtime level for the second-kind API will be private, so we are more or less free in implementing it. The easiest solution would be to develop a virtual state machine that creates a “runtime” (e.g. a stateful execution context) to run a program and control its state.
```
POST /v1/runtimes
@ -358,7 +358,7 @@ Both variants are plausible, selecting one of them depends on implementation det
#### Abstraction Levels Isolation
A crucial quality of properly separated abstraction levels (and therefore a requirement to their design) is a level isolation restriction: **only adjacent levels may interact**. If ‘jumping over’ is needed in the API design, then clearly mistakes were made.
A crucial quality of properly separated abstraction levels (and therefore a requirement to their design) is a level isolation restriction: **only adjacent levels may interact**. If “jumping over” is needed in the API design, then clearly mistakes were made.
Get back to our example. How retrieving order status would work? To obtain a status the following call chain is to be performed:
* a user initiates a call to the `GET /v1/orders` method;
@ -368,11 +368,11 @@ Get back to our example. How retrieving order status would work? To obtain a sta
* 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 a 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 ‘call chain’ wording shouldn't be treated literally. Each abstraction level might be organized differently in a technical sense:
**NB**: The “call chain” wording shouldn't be treated literally. Each abstraction level might be organized differently in a technical sense:
* there might be explicit proxying of calls down the hierarchy;
* there might be a cache at each level, being updated upon receiving a callback call or an event. In particular, a low-level runtime execution cycle obviously must be independent of upper levels, renew its state in the background, and not wait for an explicit call.
Note what happens here: each abstraction level wields its own status (e.g. order, runtime, sensors status), being formulated in subject area terms corresponding to this level. Forbidding the ‘jumping over’ results in the necessity to spawn statuses at each level independently.
Note what happens here: each abstraction level wields its own status (e.g. order, runtime, sensors status), being formulated in subject area terms corresponding to this level. Forbidding the “jumping over” results in the necessity to spawn statuses at each level independently.
Let's now look at how the order cancel operation flows through our abstraction levels. In this case, the call chain will look like that:
* a user initiates a call to the `POST /v1/orders/{id}/cancel` method;
@ -389,10 +389,10 @@ Let's now look at how the order cancel operation flows through our abstraction l
Handling state-modifying operations like the `cancel` one requires more advanced abstraction levels juggling skills compared to non-modifying calls like the `GET /status` one. There are two important moments:
1. At each abstraction level the idea of ‘order canceling’ is reformulated:
* at the `orders` level, this action in fact splits into several ‘cancels’ of other levels: you need to cancel money holding and to cancel an 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.
1. At each abstraction level the idea of “order canceling” is reformulated:
* at the `orders` level, this action in fact splits into several “cancels” of other levels: you need to cancel money holding and to cancel an 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.
2. From a high-level point of view, canceling an order is a terminal action, since no further operations are possible. From a low-level point of view, the processing continues until the cup is discarded, and then the machine is to be unlocked (e.g. new runtimes creation allowed). It's a task to the execution control level to couple those two states, outer (the order is canceled) and inner (the execution continues).
@ -410,8 +410,8 @@ What data flow do we have in our coffee API?
1. It starts with the sensors data, i.e. volumes of coffee / water / cups. This is the lowest data level we have, and here we can't change anything.
2. A continuous sensors data stream is being transformed into discrete command execution statuses, injecting new concepts which don't exist within the subject area. A coffee machine API doesn't provide a ‘coffee is being shed’ or a ‘cup is being set’ notion. It's our software that treats incoming sensors data and introduces new terms: if the volume of coffee or water is less than the target one, then the process isn't over yet. If the target value is reached, then this synthetic status is to be switched, and the next command to be executed.
It is important to note that we don't calculate new variables out from sensors data: we need to create a new dataset first, a context, an ‘execution program’ comprising a sequence of steps and conditions, and to fill it with initial values. If this context is missing, it's impossible to understand what's happening with the machine.
2. A continuous sensors data stream is being transformed into discrete command execution statuses, injecting new concepts which don't exist within the subject area. A coffee machine API doesn't provide a “coffee is being shed” or a “cup is being set” notion. It's our software that treats incoming sensors data and introduces new terms: if the volume of coffee or water is less than the target one, then the process isn't over yet. If the target value is reached, then this synthetic status is to be switched, and the next command to be executed.
It is important to note that we don't calculate new variables out from sensors data: we need to create a new dataset first, a context, an “execution program” comprising a sequence of steps and conditions, and to fill it with initial values. If this context is missing, it's impossible to understand what's happening with the machine.
3. Having logical data about the program execution state, we can (again via creating a new high-level data context) merge two different data streams from two different kinds of APIs into a single stream, which provides in a unified form the data regarding executing a beverage preparation program with logical variables like the recipe, volume, and readiness status.
@ -425,8 +425,8 @@ We may also traverse the tree backward.
3. At the runtime level, we read the target parameters (which operation to execute, and what the target volume is) and translate them into coffee machine API microcommands and statuses for each command.
Also, if we take a deeper look into the ‘bad’ decision (forcing developers to determine actual order status on their own), being discussed at the beginning of this chapter, we could notice a data flow collision there:
* from one side, in the order context ‘leaked’ physical data (beverage volume prepared) is injected, therefore stirring abstraction levels irreversibly;
Also, if we take a deeper look into the “bad” decision (forcing developers to determine actual order status on their own), being discussed at the beginning of this chapter, we could notice a data flow collision there:
* from one side, in the order context “leaked” physical data (beverage volume prepared) is injected, therefore stirring abstraction levels irreversibly;
* from the other side, 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, from one side, helps us to separate abstraction levels properly, and, from the other side, to check if our theoretical structures work as intended.

View File

@ -5,9 +5,9 @@ Based on the previous chapter, we understand that the abstraction hierarchy in o
* 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 in 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.
We are now to define each entity's responsibility area: what's the reasoning in 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 the first connected context terms (‘a lungo ordered by a user’) to the second connect context terms (‘a command performed by a coffee machine’).
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 the first connected context terms (“a lungo ordered by a user”) to the second connect context terms (“a command performed by a coffee machine”).
As for our fictional example, it would look as follows.
1. User-level entities.
@ -16,11 +16,11 @@ As for our fictional example, it would look as follows.
* checked for its status;
* retrieved;
* canceled;
* A `recipe` describes an ‘ideal model’ of some coffee beverage type, e.g. its customer properties. A `recipe` is an immutable entity for us, which means we could only read it.
* A `recipe` describes an “ideal model” of some coffee beverage type, e.g. its customer properties. A `recipe` is an immutable entity for us, which means we could only read it.
* A `coffee-machine` is a model of a real-world device. We must be able to retrieve the coffee machine's geographical location and the options it supports from this model (which will be discussed below).
2. Program execution control level entities.
* A `program` describes a general execution plan for a coffee machine. Programs could only be read.
* The `programs/matcher` entity is capable of coupling a `recipe` and a `program`, which in fact means to retrieve a dataset needed to prepare a specific recipe on a specific coffee machine.
* The `programs/matcher` entity is capable of coupling a `recipe` and a `program`, which in fact means to retrieve a dataset needed to prepare a specific recipe on a specific coffee machine.
* A `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;
@ -73,7 +73,7 @@ app.display(matchingCoffeeMachines);
As you see, developers are to write a lot of redundant code (to say nothing about the difficulties 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 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 tastes;
* 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 tastes;
* 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:
@ -106,7 +106,7 @@ Here:
* an `offer` — is a marketing bid: on what conditions a user could have the requested coffee beverage (if specified in the request), or some kind of a marketing offer — prices for the most popular or interesting products (if no specific preference was set);
* a `place` — is a spot (café, restaurant, street vending machine) where the coffee machine is located; we never introduced this entity before, but it's quite obvious that users need more convenient guidance to find a proper coffee machine than just geographical coordinates.
**NB**. We could have enriched the existing `/coffee-machines` endpoint instead of adding a new one. This decision, however, looks less semantically viable: coupling in one interface different modes of listing entities, by relevance and by order, is usually a bad idea because these two types of rankings imply different usage features and scenarios. Furthermore, enriching the search with ‘offers’ pulls this functionality out of the `coffee-machines` namespace: the fact of getting offers to prepare specific beverages in specific conditions is a key feature to users, with specifying the coffee machine being just a part of an offer.
**NB**. We could have enriched the existing `/coffee-machines` endpoint instead of adding a new one. This decision, however, looks less semantically viable: coupling in one interface different modes of listing entities, by relevance and by order, is usually a bad idea because these two types of rankings imply different usage features and scenarios. Furthermore, enriching the search with “offers” pulls this functionality out of the `coffee-machines` namespace: the fact of getting offers to prepare specific beverages in specific conditions is a key feature to users, with specifying the coffee machine being just a part of an offer.
Coming back to the code developers are writing, it would now look like that:
```
@ -119,14 +119,14 @@ app.display(offers);
#### Helpers
Methods similar to the newly invented `offers/search` one are called *helpers*. The purpose they exist is to generalize known API usage scenarios and facilitate implementing them. By ‘facilitating’ we mean not only reducing wordiness (getting rid of ‘boilerplates’) but also helping developers to avoid common problems and mistakes.
Methods similar to the newly invented `offers/search` one are called *helpers*. The purpose they exist is to generalize known API usage scenarios and facilitate implementing them. By “facilitating” we mean not only reducing wordiness (getting rid of “boilerplates”) but also helping developers to avoid common problems and mistakes.
For instance, let's consider the order price question. Our search function returns some ‘offers’ with prices. But ‘price’ is volatile; coffee could cost less during ‘happy hours’, for example. Developers could make a mistake thrice while implementing this functionality:
For instance, let's consider the order price question. Our search function returns some “offers” with prices. But “price” is volatile; coffee could cost less during “happy hours,” for example. Developers could make a mistake thrice while implementing this functionality:
* cache search results on a client device for too long (as a result, the price will always be nonactual);
* contrary to previous, call search method excessively just to actualize prices, thus overloading the network and the API servers;
* create an order with an invalid price (therefore deceiving a user, displaying one sum, and debiting another).
To solve the third problem we could demand including the displayed price in the order creation request, and return an error if it differs from the actual one. (In fact, any API working with money *shall* do so.) But it isn't helping with the first two problems and makes the user experience degrade. Displaying the actual price is always a much more convenient behavior than displaying errors upon pressing the ‘place an order’ button.
To solve the third problem we could demand including the displayed price in the order creation request, and return an error if it differs from the actual one. (In fact, any API working with money *shall* do so.) But it isn't helping with the first two problems and makes the user experience degrade. 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.
```
@ -149,13 +149,13 @@ One solution is to provide a special identifier to an offer. This identifier mus
"cursor"
}
```
By doing so we're not only helping developers to grasp the concept of getting the relevant price, but also solving a UX task of telling users about ‘happy hours’.
By doing so we're not only helping developers to grasp the concept of getting the relevant price, but also solving a UX task of telling users about “happy hours.”
As an alternative, we could split endpoints: one for searching, another one for obtaining offers. This second endpoint would only be needed to actualize prices in the specified places.
#### Error Handling
And one more step towards making developers' life easier: how an ‘invalid price’ error would look like?
And one more step towards making developers' life easier: how an “invalid price” error would look like?
```
POST /v1/orders
@ -164,7 +164,7 @@ POST /v1/orders
{ "message": "Invalid price" }
```
Formally speaking, this error response is enough: users get the ‘Invalid price’ message, and they have to repeat the order. But from a UX point of view that would be a horrible decision: the user hasn't made any mistakes, and this message isn't helpful at all.
Formally speaking, this error response is enough: users get the “Invalid price” message, and they have to repeat the order. But from a UX point of view that would be a horrible decision: the user hasn't made any mistakes, and this message isn't helpful at all.
The main rule of error interfaces in the APIs is: an error response must help a client to understand *what to do with this error*. All other stuff is unimportant: if the error response was machine-readable, there would be no need for the user-readable message.
@ -174,7 +174,7 @@ An error response content must address the following questions:
HTTP APIs traditionally employ the `4xx` status codes to indicate client problems, `5xx` to indicate server problems (with the exception of the `404` code, which is an uncertainty status).
2. If the error is caused by a server, is there any sense to repeat the request? If yes, then when?
3. If the error is caused by a client, is it resolvable, or not?
The invalid price error is resolvable: a client could obtain a new price offer and create a new order with it. But if the error occurred because of a mistake in the client code, then eliminating the cause is impossible, and there is no need to make the user push the ‘place an order’ button again: this request will never succeed.
The invalid price error is resolvable: a client could obtain a new price offer and create a new order with it. But if the error occurred because of a mistake in the client code, then eliminating the cause is impossible, and there is no need to make the user push the “place an order” button again: this request will never succeed.
**NB**: here and throughout we indicate resolvable problems with the `409 Conflict` code, and unresolvable ones with the `400 Bad Request` code.
4. If the error is resolvable, then what's the kind of problem? Obviously, a client couldn't resolve a problem it's unaware of. For every resolvable problem, some *code* must be written (reobtaining the offer in our case), so a list of error descriptions must exist.
5. If the same kind of errors arise because of different parameters being invalid, then which parameter value is wrong exactly?
@ -199,15 +199,15 @@ In our case, the price mismatch error should look like this:
}
```
After getting this error, a client is to check the error's kind (‘some problem with offer’), check the specific error reason (‘order lifetime expired’), and send an offer retrieving request again. If the `checks_failed` field indicated another error reason (for example, the offer isn't bound to the specified user), client actions would be different (re-authorize the user, then get a new offer). If there were no error handlers for this specific reason, a client would show the `localized_message` to the user, and invoke the standard error recovery procedure.
After getting this error, a client is to check the error's kind (“some problem with offer”), check the specific error reason (“order lifetime expired”), and send an offer retrieving request again. If the `checks_failed` field indicated another error reason (for example, the offer isn't bound to the specified user), client actions would be different (re-authorize the user, then get a new offer). If there were no error handlers for this specific reason, a client would show the `localized_message` to the user, and invoke the standard error recovery procedure.
It is also worth mentioning that unresolvable errors are useless to a user at the time (since the client couldn't react usefully to unknown errors), but it doesn't mean that providing extended error data is excessive. A developer will read it when fixing the error in the code. Also, check paragraphs 12 and 13 in the next chapter.
#### Decomposing Interfaces. The ‘7±2’ Rule
#### Decomposing Interfaces. The “7±2” Rule
Out of our own API development experience, we can tell without any doubt that the greatest final interface design mistake (and the greatest developers' pain accordingly) is excessive overloading of entities' interfaces with fields, methods, events, parameters, and other attributes.
Meanwhile, there is the ‘Golden Rule’ of interface design (applicable not only to APIs but almost to anything): humans could comfortably keep 7±2 entities in short-term memory. Manipulating a larger number of chunks complicates things for most humans. The rule is also known as the [‘Miller's law’](https://en.wikipedia.org/wiki/Working_memory#Capacity).
Meanwhile, there is the “Golden Rule” of interface design (applicable not only to APIs but almost to anything): humans could comfortably keep 7±2 entities in short-term memory. Manipulating a larger number of chunks complicates things for most humans. The rule is also known as the [“Miller's law”](https://en.wikipedia.org/wiki/Working_memory#Capacity).
The only possible method of overcoming this law is decomposition. Entities should be grouped under a single designation at every concept level of the API, so developers are never to operate more than 10 entities at a time.
@ -301,6 +301,6 @@ Let's try to group it together:
Such decomposed API is much easier to read than a long sheet of different attributes. Furthermore, it's probably better to group even more entities in advance. For example, a `place` and a `route` could be joined in a single `location` structure, or an `offer` and a `pricing` might be combined into some generalized object.
It is important to say that readability is achieved not only by mere grouping the entities. Decomposing must be performed in such a manner that a developer, while reading the interface, instantly understands: here is the place description of no interest to me right now, no need to traverse deeper. If the data fields needed to complete some action are scattered all over different composites, the readability doesn't improve but degrades.
It is important to say that readability is achieved not only by mere grouping the entities. Decomposing must be performed in such a manner that a developer, while reading the interface, instantly understands: here is the place description of no interest to me right now, no need to traverse deeper. If the data fields needed to complete some action are scattered all over different composites, the readability doesn't improve but degrades.
Proper decomposition also helps with extending and evolving the API. We'll discuss the subject in Section II.

View File

@ -85,11 +85,11 @@ So *always* specify exactly which standard is applied. Exceptions are possible i
One particular implication of this rule is that money sums must *always* be accompanied by a currency code.
It is also worth saying that in some areas the situation with standards is so spoiled that, whatever you do, someone got upset. A ‘classical’ example is geographical coordinates order (latitude-longitude vs longitude-latitude). Alas, the only working method of fighting frustration there is the ‘Serenity Notepad’ to be discussed in Section II.
It is also worth saying that in some areas the situation with standards is so spoiled that, whatever you do, someone got upset. A “classical” example is geographical coordinates order (latitude-longitude vs longitude-latitude). Alas, the only working method of fighting frustration there is the “Serenity Notepad” to be discussed in Section II.
##### Entities must have concrete names
Avoid single amoeba-like words, such as ‘get’, ‘apply’, ‘make’, etc.
Avoid single amoeba-like words, such as “get”, “apply”, “make”, etc.
**Bad**: `user.get()` — hard to guess what is actually returned.
@ -151,7 +151,7 @@ If an entity name is a polysemantic term itself, which could confuse developers,
// coffee machine builtin functions
GET /coffee-machines/{id}/functions
```
Word ‘function’ is many-valued. It could mean built-in functions, but also ‘a piece of code’, or a state (machine is functioning).
Word “function” is many-valued. It could mean built-in functions, but also “a piece of code,” or a state (machine is functioning).
**Better**: `GET /v1/coffee-machines/{id}/builtin-functions-list`
@ -187,7 +187,7 @@ We're leaving the exercise of making these signatures better to the reader.
— humans are bad at perceiving double negation; make mistakes.
**Better**: `"prohibit_calling": true` or `"avoid_calling": true`
— it's easier to read, though you shouldn't deceive yourself. Avoid semantical double negations, even if you've found a ‘negative’ word without a ‘negative’ prefix.
— it's easier to read, though you shouldn't deceive yourself. Avoid semantical double negations, even if you've found a “negative” word without a “negative” prefix.
Also worth mentioning is that making mistakes in the [de Morgan's laws](https://en.wikipedia.org/wiki/De_Morgan's_laws) usage is even simpler. For example, if you have two flags:
@ -200,7 +200,7 @@ GET /coffee-machines/{id}/stocks
}
```
‘Coffee might be prepared’ condition would look like `has_beans && has_cup` — both flags must be true. However, if you provide the negations of both flags:
“Coffee might be prepared” condition would look like `has_beans && has_cup` — both flags must be true. However, if you provide the negations of both flags:
```
{
@ -291,7 +291,7 @@ POST /v1/users
}
```
**NB**: the contradiction with the previous rule lies in the necessity of introducing ‘negative’ flags (the ‘no limit’ flag), which we had to rename to `abolish_spending_limit`. Though it's a decent name for a negative flag, its semantics is still unobvious, and developers will have to read the docs. That's the way.
**NB**: the contradiction with the previous rule lies in the necessity of introducing “negative” flags (the “no limit” flag), which we had to rename to `abolish_spending_limit`. Though it's a decent name for a negative flag, its semantics is still unobvious, and developers will have to read the docs. That's the way.
##### No results is a result
@ -494,7 +494,7 @@ You may note that in this setup the error can't be resolved in one step: this si
#### Developing machine-readable interfaces
In pursuit of the API clarity for humans, we frequently forget that it's not developers themselves who interact with the endpoints, but the code they've written. Many concepts that work well with user interfaces, are badly suited for the program ones: specifically, developers can't make decisions based on textual information, and they can't ‘refresh’ the state in case of some confusing situation.
In pursuit of the API clarity for humans, we frequently forget that it's not developers themselves who interact with the endpoints, but the code they've written. Many concepts that work well with user interfaces, are badly suited for the program ones: specifically, developers can't make decisions based on textual information, and they can't “refresh” the state in case of some confusing situation.
##### The system state must be observable by clients
@ -578,7 +578,7 @@ This rule is applicable to errors as well, especially client ones. If the error
In modern systems, clients usually have their own state and almost universally cache results of requests — no matter, session-wise or long-term, every entity has some period of autonomous existence. So it's highly desirable to make clarifications; it should be understandable how the data is supposed to be cached, if not from operation signatures, but at least from the documentation.
Let's stress that we understand ‘cache’ in the extended sense: which variation of operation parameters (not just the request time, but other variables as well) should be considered close enough to some previous request to use the cached result?
Let's stress that we understand “cache” in the extended sense: which variation of operation parameters (not just the request time, but other variables as well) should be considered close enough to some previous request to use the cached result?
**Bad**:
@ -693,13 +693,13 @@ GET /v1/records?cursor=<cursor value>
{ "records", "cursor" }
```
One advantage of this approach is the possibility to keep initial request parameters (i.e. the `filter` in our example) embedded into the cursor itself, thus not copying them in follow-up requests. It might be especially actual if the initial request prepares the full dataset, for example, moving it from the ‘cold’ storage to a ‘hot’ one (then the `cursor` might simply contain the encoded dataset id and the offset).
One advantage of this approach is the possibility to keep initial request parameters (i.e. the `filter` in our example) embedded into the cursor itself, thus not copying them in follow-up requests. It might be especially actual if the initial request prepares the full dataset, for example, moving it from the “cold” storage to a “hot” one (then the `cursor` might simply contain the encoded dataset id and the offset).
There are several approaches to implementing cursors (for example, making a single endpoint for initial and follow-up requests and returning the first data portion in the first response). As usual, the crucial part is maintaining consistency across all such endpoints.
**NB**: some sources discourage this approach because in this case user can't see a list of all pages and can't choose an arbitrary one. We should note here that:
* such a case (pages list and page selection) exists if we deal with user interfaces; we could hardly imagine a *program* interface that needs to provide access to random data pages;
* if we still talk about an API to some application, which has a ‘paging’ user control, then a proper approach would be to prepare ‘paging’ data on the server side, including generating links to pages;
* if we still talk about an API to some application, which has a “paging” user control, then a proper approach would be to prepare “paging” data on the server side, including generating links to pages;
* cursor-based solutions don't prohibit using the `offset`/`limit` parameters; nothing could prevent us from creating a dual interface, which might serve both `GET /items?cursor=…` and `GET /items?offset=…&limit=…` requests;
* finally, if there is a necessity to provide access to arbitrary pages in the user interface, we should ask ourselves a question, which problem is being solved that way; probably, users use this functionality to find something: a specific element on the list, or the position they ended while working with the list last time; probably, we should provide more convenient controls to solve those tasks than accessing data pages by their indexes.
@ -768,7 +768,7 @@ If the protocol allows, fractional numbers with fixed precision (like money sums
If there is no Decimal type in the protocol (for instance, JSON doesn't have one), you should either use integers (e.g. apply a fixed multiplicator) or strings.
If conversion to a float number will certainly lead to losing the precision (let's say if we translate ‘20 minutes’ into hours as a decimal fraction), it's better to either stick to a fully precise format (e.g. opt for `00:20` instead of `0.33333…`) or to provide an SDK to work with this data, or as a last resort describe the rounding principles in the documentation.
If conversion to a float number will certainly lead to losing the precision (let's say if we translate “20 minutes” into hours as a decimal fraction), it's better to either stick to a fully precise format (e.g. opt for `00:20` instead of `0.33333…`) or to provide an SDK to work with this data, or as a last resort describe the rounding principles in the documentation.
##### All API operations must be idempotent
@ -849,7 +849,7 @@ X-Idempotency-Token: <token>
```
— the server found out that a different token was used in creating revision 124, which means an access conflict.
Furthermore, adding idempotency tokens not only resolves the issue but also makes advanced optimizations possible. If the server detects an access conflict, it could try to resolve it, ‘rebasing’ the update like modern version control systems do, and return a `200 OK` instead of a `409 Conflict`. This logic dramatically improves user experience, being fully backwards compatible, and helps to avoid conflict-resolving code fragmentation.
Furthermore, adding idempotency tokens not only resolves the issue but also makes advanced optimizations possible. If the server detects an access conflict, it could try to resolve it, “rebasing” the update like modern version control systems do, and return a `200 OK` instead of a `409 Conflict`. This logic dramatically improves user experience, being fully backwards compatible, and helps to avoid conflict-resolving code fragmentation.
Also, be warned: clients are bad at implementing idempotency tokens. Two problems are common:
* you can't really expect that clients generate truly random tokens — they may share the same seed or simply use weak algorithms or entropy sources; therefore you must put constraints on token checking: token must be unique to a specific user and resource, not globally;
@ -915,7 +915,7 @@ api.updateRecipes({
}]
});
// You may actually return
// a ‘partial success’ status
// a “partial success” status
// if the protocol allows it
{
@ -997,7 +997,7 @@ api.updateRecipes({
}
```
To the client, everything looks normal: changes were applied, and the last response got is always actual. But the resource state after the first request was inherently different from the resource state after the second one, which contradicts the very definition of ‘idempotency’.
To the client, everything looks normal: changes were applied, and the last response got is always actual. But the resource state after the first request was inherently different from the resource state after the second one, which contradicts the very definition of “idempotency.”
It would be more correct if the server did nothing upon getting the second request with the same idempotency token, and returned the same status list breakdown. But it implies that storing these breakdowns must be implemented.
@ -1039,7 +1039,7 @@ If the first two problems are solved by applying pure technical measures (see th
* if you expect a significant number of asynchronous operations in the API, allow developers to choose between the poll model (clients make repeated requests to an endpoint to check the asynchronous procedure status) and the push model (the server notifies clients of status changes, for example, via webhooks or server-push mechanics);
* if some entity comprises both ‘lightweight’ data (let's say, the name and the description of the recipe) and ‘heavy’ data (let's say, the promo picture of the beverage which might easily be a hundred times larger than the text fields), it's better to split endpoints and pass only a reference to the ‘heavy’ data (a link to the image, in our case) — this will allow at least setting different cache policies for different kinds of data.
* if some entity comprises both “lightweight” data (let's say, the name and the description of the recipe) and “heavy” data (let's say, the promo picture of the beverage which might easily be a hundred times larger than the text fields), it's better to split endpoints and pass only a reference to the “heavy” data (a link to the image, in our case) — this will allow at least setting different cache policies for different kinds of data.
As a useful exercise, try modeling the typical lifecycle of a partner's app's main functionality (for example, making a single order) to count the number of requests and the amount of traffic that it takes.
@ -1081,7 +1081,7 @@ PATCH /v1/orders/{id}
This signature is bad per se as it's unreadable. What does `null` as the first array element mean — is it a deletion of an element or an indication that no actions are needed towards it? What happens with the fields that are not stated in the update operation body (`delivery_address`, `milk_type`) — will they be reset to defaults, or stay unchanged?
The nastiest part is that whatever option you choose, the number of problems will only multiply further. Let's say we agreed that the `{"items":[null, {…}]}` statement means that the first element of the array is left untouched, e.g. no changes are needed. Then, how shall we encode its deletion? Invent one more ‘magical’ value meaning ‘remove it’? Similarly, if the fields that are not explicitly mentioned retain their value — how to reset them to defaults?
The nastiest part is that whatever option you choose, the number of problems will only multiply further. Let's say we agreed that the `{"items":[null, {…}]}` statement means that the first element of the array is left untouched, e.g. no changes are needed. Then, how shall we encode its deletion? Invent one more “magical” value meaning “remove it”? Similarly, if the fields that are not explicitly mentioned retain their value — how to reset them to defaults?
**The simple solution** is always rewriting the data entirely, e.g. to require passing the entire object, to replace the current state with it, and to return the full state as a result of the operation. This obvious solution is frequently rejected with the following reasoning:
* increased requests sizes and therefore, the amount of traffic;
@ -1182,7 +1182,7 @@ X-Idempotency-Token: <idempotency token>
}
```
This approach is much harder to implement, but it's the only viable method to implement collaborative editing since it explicitly reflects what a user was actually doing with entity representation. With data exposed in such a format, you might actually implement offline editing, when user changes are accumulated and then sent at once, while the server automatically resolves conflicts by ‘rebasing’ the changes.
This approach is much harder to implement, but it's the only viable method to implement collaborative editing since it explicitly reflects what a user was actually doing with entity representation. With data exposed in such a format, you might actually implement offline editing, when user changes are accumulated and then sent at once, while the server automatically resolves conflicts by “rebasing” the changes.
#### Ensuring API product quality
@ -1200,7 +1200,7 @@ One important implication: **never use increasing numbers as external identifier
##### Stipulate future restrictions
With the API popularity growth, it will inevitably become necessary to introduce technical means of preventing illicit API usage, such as displaying captchas, setting honeypots, raising the ‘too many requests’ exceptions, installing anti-DDoS proxies, etc. All these things cannot be done if the corresponding errors and messages were not described in the docs from the very beginning.
With the API popularity growth, it will inevitably become necessary to introduce technical means of preventing illicit API usage, such as displaying captchas, setting honeypots, raising the “too many requests” exceptions, installing anti-DDoS proxies, etc. All these things cannot be done if the corresponding errors and messages were not described in the docs from the very beginning.
You are not obliged to actually generate those exceptions, but you might stipulate this possibility in the terms of service. For example, you might describe the `429 Too Many Requests` error or captcha redirect, but implement the functionality when it's actually needed.
@ -1218,11 +1218,11 @@ All endpoints must accept language parameters (for example, in a form of the `Ac
It is important to understand that the user's language and the user's jurisdiction are different things. Your API working cycle must always store the user's location. It might be stated either explicitly (requests contain geographical coordinates) or implicitly (initial location-bound request initiates session creation which stores the location), but no correct localization is possible in absence of location data. In most cases reducing the location to just a country code is enough.
The thing is that lots of parameters potentially affecting data formats depend not on language, but on a user's location. To name a few: number formatting (integer and fractional part delimiter, digit groups delimiter), date formatting, the first day of the week, keyboard layout, measurement units system (which might be non-decimal!), etc. In some situations, you need to store two locations: user residence location and user ‘viewport’. For example, if a US citizen is planning a European trip, it's convenient to show prices in local currency, but measure distances in miles and feet.
The thing is that lots of parameters potentially affecting data formats depend not on language, but on a user's location. To name a few: number formatting (integer and fractional part delimiter, digit groups delimiter), date formatting, the first day of the week, keyboard layout, measurement units system (which might be non-decimal!), etc. In some situations, you need to store two locations: user residence location and user “viewport.” For example, if a US citizen is planning a European trip, it's convenient to show prices in local currency, but measure distances in miles and feet.
Sometimes explicit location passing is not enough since there are lots of territorial conflicts in the world. How the API should behave when user coordinates lie within disputed regions is a legal matter, regretfully. The author of this book once had to implement a ‘state A territory according to state B official position’ concept.
Sometimes explicit location passing is not enough since there are lots of territorial conflicts in the world. How the API should behave when user coordinates lie within disputed regions is a legal matter, regretfully. The author of this book once had to implement a “state A territory according to state B official position” concept.
**Important**: mark a difference between localization for end users and localization for developers. Take a look at the example in rule \#12: the `localized_message` is meant for the user; the app should show it if there is no specific handler for this error exists in the code. This message must be written in the user's language and formatted according to the user's location. But the `details.checks_failed[].message` is meant to be read by developers examining the problem. So it must be written and formatted in a manner that suits developers best. In the software development world, it usually means ‘in English’.
**Important**: mark a difference between localization for end users and localization for developers. Take a look at the example in rule \#12: the `localized_message` is meant for the user; the app should show it if there is no specific handler for this error exists in the code. This message must be written in the user's language and formatted according to the user's location. But the `details.checks_failed[].message` is meant to be read by developers examining the problem. So it must be written and formatted in a manner that suits developers best. In the software development world, it usually means “in English.”
Worth mentioning is that the `localized_` prefix in the example is used to differentiate messages to users from messages to developers. A concept like that must be, of course, explicitly stated in your API docs.

View File

@ -1,14 +1,14 @@
### The Backwards Compatibility Problem Statement
As usual, let's conceptually define ‘backwards compatibility’ before we start.
As usual, let's conceptually define “backwards compatibility” before we start.
Backwards compatibility is a feature of the entire API system to be stable in time. It means the following: **the code that developers have written using your API continues working functionally correctly for a long period of time**. There are two important questions to this definition and two explanations:
1. What does ‘functionally correctly’ mean?
1. What does “functionally correctly” mean?
It means that the code continues to serve its function, e.g. solve some users' problems. It doesn't mean it continues working indistinguishably: for example, if you're maintaining a UI library, changing functionally insignificant design details like shadow depth or border stoke type is backwards compatible, whereas changing visual components size is not.
2. What does ‘a long period of time’ mean?
2. What does “a long period of time” mean?
From our point of view, the backwards compatibility maintenance period should be reconciled with the subject area application lifetime. Platform LTS periods are decent guidance in most cases. Since apps will be rewritten anyway when the platform maintenance period ends, it is reasonable to expect developers to move to the new API version also. In mainstream subject areas (e.g. desktop and mobile operating systems) this period lasts several years.
@ -16,7 +16,7 @@ From the definition becomes obvious why backwards compatibility needs to be main
But let's take a look at the problem from another angle: why the maintaining backwards compatibility problem exists at all? Why would anyone *want* to break it? This question, though it looks quite trivial, is much more complicated than the previous one.
We could say the *we break backwards compatibility to introduce new features to the API*. But that would be deceiving: new features are called *‘new’* just because they cannot affect existing implementations which are not using them. We must admit there are several associated problems, which lead to the aspiration to rewrite *our* code, the code of the API itself, and ship a new major version:
We could say the *we break backwards compatibility to introduce new features to the API*. But that would be deceiving: new features are called *“new”* just because they cannot affect existing implementations which are not using them. We must admit there are several associated problems, which lead to the aspiration to rewrite *our* code, the code of the API itself, and ship a new major version:
* the code eventually becomes outdated; making changes, even introducing totally new functionality, is impractical;
@ -24,9 +24,9 @@ We could say the *we break backwards compatibility to introduce new features to
* finally, with years passing since the initial release, we understood more about the subject area and API usage best practices, and we would implement many things differently.
These arguments could be summarized frankly as the API developers don't want to support the old code. But this explanation is still incomplete: even if you're not going to rewrite the API code to add new functionality, or you're not going to add it at all, you still have to ship new API versions, minor and major alike.
These arguments could be summarized frankly as the API developers don't want to support the old code. But this explanation is still incomplete: even if you're not going to rewrite the API code to add new functionality, or you're not going to add it at all, you still have to ship new API versions, minor and major alike.
**NB**: in this chapter, we don't make any difference between minor versions and patches: ‘minor version’ means any backwards-compatible API release.
**NB**: in this chapter, we don't make any difference between minor versions and patches: “minor version” means any backwards-compatible API release.
Let us remind that [an API is a bridge]((https://twirl.github.io/The-API-Book/docs/API.en.html#chapter-2)), a meaning of connecting different programmable contexts. No matter how strong our desire to keep the bridge intact is, our capabilities are limited: we could lock the bridge, but we cannot command the rifts and the canyon itself. That's the source of the problems: we can't guarantee that *our own* code won't change, so at some point, we will have to ask the clients to change *their* code.
@ -43,12 +43,12 @@ When you shipped the very first API version, and the first clients started to us
2. If the code-on-demand feature isn't supported or is prohibited by the platform, as in modern mobile operating systems, then the situation becomes more severe. Each client effectively borrows a snapshot of the code, working with your API, frozen at the moment of compilation. Client application updates are scattered over time at much more extent than Web application updates. The most painful thing is that *some clients will never be up to date*, because of one of the three reasons:
* developers simply don't want to update the app, e.g. its development stopped;
* users don't want to get updates (sometimes because users think that developers ‘spoiled’ the app in new versions);
* users don't want to get updates (sometimes because users think that developers “spoiled” the app in new versions);
* users can't get updates because their devices are no longer supported.
In modern times these three categories combined could easily constitute tens of per cent of auditory. It implies that cutting the support of any API version might be remarkable — especially if developers' apps continue supporting a more broad spectrum of platforms than the API does.
You could have never issued any SDK, providing just the server-side API, for example in a form of HTTP endpoints. You might think, given your API is less competitive on the market because of a lack of SDKs, that the backwards compatibility problem is mitigated. That's not true: if you don't provide an SDK, then developers will either adopt an unofficial one (if someone bothers to make it) or just write a framework themselves — independently. ‘Your framework — your problems’ strategy, fortunately or not, works badly: if developers write poor quality code upon your API, then your API is of poor quality itself. Definitely in the view of developers, possibly in the view of end-users, if the API performance within the app is visible to them.
You could have never issued any SDK, providing just the server-side API, for example in a form of HTTP endpoints. You might think, given your API is less competitive on the market because of a lack of SDKs, that the backwards compatibility problem is mitigated. That's not true: if you don't provide an SDK, then developers will either adopt an unofficial one (if someone bothers to make it) or just write a framework themselves — independently. “Your framework — your problems” strategy, fortunately or not, works badly: if developers write poor quality code upon your API, then your API is of poor quality itself. Definitely in the view of developers, possibly in the view of end-users, if the API performance within the app is visible to them.
Certainly, if you provide a stateless API that doesn't require client SDKs (or they might be auto-generated from the spec), those problems will be much less noticeable, but not fully avoidable, unless you never issue any new API version. If you do, you will still have to deal with some fragmentation of users by API and SDK versions.
@ -66,11 +66,11 @@ Let us also stress that low-level API vendors are not always as resolute regardi
#### Platform drift
Finally, there is a third side to a story — the ‘canyon’ you're crossing over with a bridge of your API. Developers write code that is executed in some environment you can't control, and it's evolving. New versions of operating systems, browsers, protocols, and programming language SDKs emerge. New standards are being developed, new arrangements made, some of them being backwards-incompatible, and nothing could be done about that.
Finally, there is a third side to a story — the “canyon” you're crossing over with a bridge of your API. Developers write code that is executed in some environment you can't control, and it's evolving. New versions of operating systems, browsers, protocols, and programming language SDKs emerge. New standards are being developed, new arrangements made, some of them being backwards-incompatible, and nothing could be done about that.
Older platform versions lead to fragmentation just like older app versions do, because developers (including the API developers) are struggling with supporting older platforms, and users are struggling with platform updates — and often can't update at all, since newer platform versions require newer devices.
The nastiest thing here is that not only does incremental progress in a form of new platforms and protocols demand changing the API, but also does vulgar fashion. Several years ago realistic 3d icons were popular, but since then the public taste changed in a favor of flat and abstract ones. UI components developers had to follow the fashion, rebuilding their libraries, either shipping new icons or replacing old ones. Similarly, right now ‘night mode’ support is introduced everywhere, demanding changes in a broad range of APIs.
The nastiest thing here is that not only does incremental progress in a form of new platforms and protocols demand changing the API, but also does vulgar fashion. Several years ago realistic 3d icons were popular, but since then the public taste changed in a favor of flat and abstract ones. UI components developers had to follow the fashion, rebuilding their libraries, either shipping new icons or replacing old ones. Similarly, right now “night mode” support is introduced everywhere, demanding changes in a broad range of APIs.
#### Backwards compatibility policy
@ -100,7 +100,7 @@ Let's briefly describe these decisions and the key factors for making them.
In modern professional software development, especially if we talk about internal APIs, a new API version usually fully replaces the previous one. If some problems are found, it might be rolled back (by releasing the previous version), but the two builds never co-exist. However, in the case of public APIs, the more the number of partner integrations is, the more dangerous this approach becomes.
Indeed, with the growth of the number of users, the ‘rollback the API version in case of problems’ paradigm becomes increasingly destructive. To a partner, the optimal solution is rigidly referencing the specific API version — the one that had been tested (ideally, at the same time having the API vendor somehow seamlessly fixing security issues and making their software compliant with newly introduced legislation).
Indeed, with the growth of the number of users, the “rollback the API version in case of problems” paradigm becomes increasingly destructive. To a partner, the optimal solution is rigidly referencing the specific API version — the one that had been tested (ideally, at the same time having the API vendor somehow seamlessly fixing security issues and making their software compliant with newly introduced legislation).
**NB**. From the same considerations, providing beta (or maybe even alpha) versions of the popular APIs becomes more and more desirable as well, to make partners test the upcoming version and address the possible issues in advance.
@ -112,4 +112,4 @@ The important (and undeniable) advantage of the *semver* system is that it provi
Of course, preserving minor versions infinitely isn't possible (partly because of security and compliance issues that tend to pile up). However, providing such access for a reasonable period of time is rather a hygienic norm for popular APIs.
**NB**. Sometimes to defend the single accessible API version concept, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backwards compatibility, as it might be relying on some un-versioned services (for example, some data in the DB that are shared between all the API versions). We, however, consider this an additional reason to isolate such dependencies (see ‘The Serenity Notepad’ chapter) as it means that changes to these subsystems might lead to the inoperability of the API.
**NB**. Sometimes to defend the single accessible API version concept, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backwards compatibility, as it might be relying on some un-versioned services (for example, some data in the DB that are shared between all the API versions). We, however, consider this an additional reason to isolate such dependencies (see “The Serenity Notepad” chapter) as it means that changes to these subsystems might lead to the inoperability of the API.

View File

@ -8,13 +8,13 @@ At any moment in its lifetime, your API is like an iceberg: it comprises an obse
* 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.
* 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.
Rule \#1 is the simplest: if some functionality might be withheld — then never expose it. It might be reformulated like: every entity, every field, and every public API method is a *product solution*. There must be solid *product* reasons why some functionality is exposed.
##### 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. Policy on such ‘findings’ must be articulated explicitly. At the very least, in case of such non-authorized usage of undocumented functionality, you might refer to the docs, and be in your own rights in the eyes of the community.
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. Policy on such “findings” must be articulated explicitly. At the very least, in case of such non-authorized usage of undocumented functionality, you might refer to the docs, and be in your own rights in the eyes of the community.
However, API developers often legitimize such gray zones themselves, for example, by:
@ -39,7 +39,7 @@ Let's imagine that you're struggling with scaling your service, and at some poin
What would be the result? The code above will stop working. A developer creates an order, then tries to get its status — but gets the error. It's very hard to predict what approach developers would implement to tackle this error. Probably, none at all.
You may say something like, ‘But we've never promised the strict consistency in the first place’ — and that is obviously not true. You may say that if, and only if, you have really described the eventual consistency in the `createOrder` docs, and all your SDK examples look like:
You may say something like, “But we've never promised the strict consistency in the first place” — and that is obviously not true. You may say that if, and only if, you have really described the eventual consistency in the `createOrder` docs, and all your SDK examples look like:
```
let order = api.createOrder();
@ -139,6 +139,6 @@ This example leads us to the last rule.
State transition graph, event order, possible causes of status changes — such critical things must be documented. Not every piece of business logic might be defined in a form of a programmatical contract; some cannot be represented at all.
Imagine that one day you start to take phone calls. A client may contact the call center to cancel an order. You might even make this functionality *technically* backwards-compatible, 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. Partner's business analytical code might be broken likewise, or start displaying weather on Mars since it was written knowing nothing about the possibility of canceling orders somehow in circumvention of the partner's systems.
Imagine that one day you start to take phone calls. A client may contact the call center to cancel an order. You might even make this functionality *technically* backwards-compatible, 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. Partner's business analytical code might be broken likewise, or start displaying weather on Mars since it was written knowing nothing about the possibility of canceling orders somehow 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 may only cancel those orders which were created with this flag set. But that would be a bad decision from a *product* point of view. 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’ to be discussed in the last chapter of this Section.
A *technically* correct decision would be to add a “canceling via call center allowed” parameter to the order creation function. Conversely, call center operators may only cancel those orders which were created with this flag set. But that would be a bad decision from a *product* point of view. 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” to be discussed in the last chapter of this Section.

View File

@ -2,7 +2,7 @@
In previous chapters, we have tried to outline theoretical rules and illustrate them with practical examples. However, understanding the principles of change-proof API design requires practice above all things. An ability to anticipate future growth problems comes from a handful of grave mistakes once made. One cannot foresee everything but can develop certain technical intuition.
So in the following chapters, we will try to probe [our study API](#chapter-12) from the previous Section, testing its robustness from every possible viewpoint, thus carrying out some ‘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.
So in the following chapters, we will try to probe [our study API](#chapter-12) from the previous Section, testing its robustness from every possible viewpoint, thus carrying out some “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.
@ -57,13 +57,13 @@ The universal approach to making such amendments is: to consider the existing in
1. Explicitly define the programmatical contract *as it works right now*.
2. Extend the functionality: add a new method allowing for tackling those restrictions set in the previous paragraph.
3. Pronounce the existing interfaces (those defined in \#1) being ‘helpers’ to new ones (those defined in \#2) which sets some options to default values.
3. Pronounce the existing interfaces (those defined in \#1) being “helpers” to new ones (those defined in \#2) which sets some options to default values.
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:
2. Add new “with-options” endpoint:
```
PUT /v1/partners/{partner_id}⮠
/coffee-machines-with-options
@ -88,7 +88,7 @@ Usually, just adding a new optional parameter to the existing interface is enoug
#### 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 ridiculously: 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.
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 ridiculously: 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. From one side, we want developers to write neat and laconic code, so we must provide useful helpers and defaults. On the other side, we can't know in advance which sets of options will be the most frequent after several years of the API expansion.

View File

@ -1,6 +1,6 @@
### 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? There is a catch in this question: 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 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? There is a catch in this question: 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 to register the partner's own recipe:
@ -37,7 +37,7 @@ The first problem is obvious to those who read [chapter 11](#chapter-11-paragrap
]
```
And here the first big question arises: what should we do with the `default_volume` field? From one side, that's an objective quality 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 volume not like ‘300 ml’, but ‘10 fl oz’. We may propose two solutions:
And here the first big question arises: what should we do with the `default_volume` field? From one side, that's an objective quality 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 volume 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.
@ -46,11 +46,11 @@ The flaw in the first option is that a partner might be willing to use the servi
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 are setting the requirements to the API based on some very 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’ actually, 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, ‘our best quality™ coffee’ or ‘Invigorating Morning Freshness®’ designation would look very weird in between ‘Cappuccino’, ‘Lungo’, and ‘Latte’.
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 are setting the requirements to the API based on some very 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” actually, 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, “our best quality™ coffee” or “Invigorating Morning Freshness®” designation 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 a beverage, its energy value, allergen information, etc. `product_properties` 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.
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**.
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 rule of contexts
@ -80,7 +80,7 @@ PUT /formatters/volume/ru
"template": "{volume} мл"
}
// Add a specific formatting rule
// for Russian language in the ‘US’ region
// for Russian language in the “US” region
PUT /formatters/volume/ru/US
{
// in US we need to recalculate
@ -95,7 +95,7 @@ PUT /formatters/volume/ru/US
**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 `name` and `description` 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.
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 `name` and `description` 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.
```
GET /v1/layouts/{layout_id}
@ -161,7 +161,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 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:
```
POST /v1/recipe-builder
@ -214,4 +214,4 @@ POST /v1/recipes/custom
}
```
Also note that this format allows us to maintain an important extensibility point: different partners might have totally isolated namespaces, or conversely share them. Furthermore, we might introduce special namespaces (like ‘common’, for example) to allow for publishing new recipes for everyone (and that, by the way, would allow us to organize our own backoffice to edit recipes).
Also note that this format allows us to maintain an important extensibility point: different partners might have totally isolated namespaces, or conversely share them. Furthermore, we might introduce special namespaces (like `common`, for example) to allow for publishing new recipes for everyone (and that, by the way, would allow us to organize our own backoffice to edit recipes).

View File

@ -1,6 +1,6 @@
### Weak Coupling
In the previous chapter we've demonstrated how breaking the strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. A mindful reader might have noted that this technique was already used in our API study much earlier in [Chapter 9](#chapter-9) with regards to the ‘program’ and ‘program run’ entities. Indeed, we might do it without the `program-matcher` endpoint and make it this way:
In the previous chapter we've demonstrated how breaking the strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. A mindful reader might have noted that this technique was already used in our API study much earlier in [Chapter 9](#chapter-9) with regards to the “program” and “program run” entities. Indeed, we might do it without the `program-matcher` endpoint and make it this way:
```
GET /v1/recipes/{id}/run-data/{api_type}
@ -15,7 +15,7 @@ Then developers would have to make this trick to get coffee prepared:
* get the execution description, as stated above;
* depending on the API type, run some 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.
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.
But let us return to the question we have previously mentioned in [Chapter 15](#chapter-15): how should we parametrize the order preparation process implemented via third-party API. In other words, what's this `program_execution_endpoint` that we ask upon the API type registration?
@ -63,7 +63,7 @@ Though this API looks absolutely universal, it's quite easy to demonstrate how o
We may easily disprove the \#2 principle, and that will uncover the implications of the \#1. For the beginning, let us imagine that on a course of further service growth we decided to allow end-users to change the order after the execution started. For example, ask for a cinnamon sprinkling or contactless takeout. That would lead us to creating a new endpoint, let's say, `program_modify_endpoint`, and new difficulties in data format development (we need to understand in the real-time, could we actually sprinkle cinnamon on this specific cup of coffee or not). What *is* important is that both endpoint and new data fields would be optional because of backwards compatibility requirement.
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 via our API not a coffee house, but a vending machine? From one side, it means that the `modify` endpoint and all related stuff are simply meaningless: a vending machine couldn't sprinkle cinnamon over a coffee cup, and the contactless takeout requirement means nothing to it. On the other side, the machine, unlike the people-operated café, requires *takeout approval*: the end-user places an order 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 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.
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 via our API not a coffee house, but a vending machine? From one side, it means that the `modify` endpoint and all related stuff are simply meaningless: a vending machine couldn't sprinkle cinnamon over a coffee cup, and the contactless takeout requirement means nothing to it. On the other side, the machine, unlike the people-operated café, requires *takeout approval*: the end-user places an order 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 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 three endpoints:
* to have vending machines integrated a partner must implement the `program_takeout_endpoint`, but doesn't actually need the `program_modify_endpoint`;
@ -71,16 +71,16 @@ Programmable takeout approval requires one more endpoint, let's say, `program_ta
Furthermore, we have to describe both endpoints in the docs. It's quite natural that the `takeout` endpoint is very specific; unlike cinnamon sprinkling, 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.
We actually don't know, whether in the real world of coffee machine APIs this problem will really occur or not. But we can say with all confidence regarding ‘bare metal’ integrations that the processes we described *always* happen. The underlying technology shifts; an API that seemed clear and straightforward, becomes a trash bin full of legacy methods, half of which borrows no practical sense under any specific set of conditions. If we add technical progress to the situation, i.e. imagine that after a while all coffee houses become automated, we will finally end up with the situation with half of the methods *isn't actually needed at all*, like the requesting a contactless takeout one.
We actually don't know, whether in the real world of coffee machine APIs this problem will really occur or not. But we can say with all confidence regarding “bare metal” integrations that the processes we described *always* happen. The underlying technology shifts; an API that seemed clear and straightforward, becomes a trash bin full of legacy methods, half of which borrows no practical sense under any specific set of conditions. If we add technical progress to the situation, i.e. imagine that after a while all coffee houses become automated, we will finally end up with the situation with half of the methods *isn't actually needed at all*, like the requesting a contactless takeout one.
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.
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 from both sides? 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, sprinkle with cinnamon, allow this user to take it;
* the underlying program execution API level doesn't care what other same-level implementations exist; it just interprets those parts of the task which 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 Chapter 9](#chapter-9).
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 Chapter 9](#chapter-9).
In our case we need to implement the following mechanisms:
* running a program creates a corresponding context comprising all the essential parameters;
@ -137,7 +137,7 @@ It's ultimately possible that both sides would know nothing about each other and
Worth mentioning that the number of entities (fields, events), though effectively doubled compared to strong-coupled API design, increased qualitatively, not quantitatively. The `program` context describes fields and events in its own terms (type of beverage, volume, cinnamon sprinkling), while the `execution` context must reformulate those terms according to its own subject area (omitting redundant ones, by the way). It is also important that the `execution` context might concretize these properties for underlying objects according to their own specifics, while the `program` context must keep its properties general enough to be applicable to any possible underlying technology.
One more important feature of weak coupling is that it allows an entity to have several higher-level contexts. In typical subject areas, such a situation would look like an API design flaw, but in complex systems, with several system state-modifying agents present, such design patterns are not that rare. Specifically, you would likely face it while developing user-facing UI libraries. We will cover this issue in detail in the upcoming ‘SDK’ section of this book.
One more important feature of weak coupling is that it allows an entity to have several higher-level contexts. In typical subject areas, such a situation would look like an API design flaw, but in complex systems, with several system state-modifying agents present, such design patterns are not that rare. Specifically, you would likely face it while developing user-facing UI libraries. We will cover this issue in detail in the upcoming “SDK” section of this book.
#### The Inversion of Responsibility

View File

@ -4,15 +4,15 @@ Let us summarize what we have written in the three previous chapters.
1. Extending API functionality is realized through abstracting: the entity nomenclature is to be reinterpreted so that existing methods become partial (ideally — the most frequent) simplified cases to more general functionality.
2. Higher-level entities are to be the informational contexts for low-level ones, e.g. don't prescribe any specific behavior but translate their state and expose functionality to modify it (directly through calling some methods or indirectly through firing events).
3. Concrete functionality, e.g. working with ‘bare metal’ hardware or underlying platform APIs, should be delegated to low-level entities.
3. Concrete functionality, e.g. working with “bare metal” hardware or underlying platform APIs, should be delegated to low-level entities.
**NB**. There is nothing novel about these rules: one might easily recognize them being the [SOLID](https://en.wikipedia.org/wiki/SOLID) architecture principles. There is no surprise in that either, because SOLID concentrates on contract-oriented development, and APIs are contracts by definition. We've just added ‘abstraction levels’ and ‘informational contexts’ concepts there.
**NB**. There is nothing novel about these rules: one might easily recognize them being the [SOLID](https://en.wikipedia.org/wiki/SOLID) architecture principles. There is no surprise in that either, because SOLID concentrates on contract-oriented development, and APIs are contracts by definition. We've just added “abstraction levels” and “informational contexts” concepts there.
However, there is an unanswered question: how should we design the entity nomenclature from the beginning so that extending the API won't make it a mess of different inconsistent methods of different ages. The answer is pretty obvious: to avoid clumsy situations while abstracting (as with the coffee machine's supported options), all the entities must be originally considered being a specific implementation of a more general interface, even if there are no planned alternative implementations for them.
For example, we should have asked ourselves a question while designing the `POST /search` API: what is a ‘search result’? What abstract interface does it implement? To answer this question we must neatly decompose this entity to find which facet of it is used for interacting with which objects.
For example, we should have asked ourselves a question while designing the `POST /search` API: what is a “search result”? What abstract interface does it implement? To answer this question we must neatly decompose this entity to find which facet of it is used for interacting with which objects.
Then we would have come to the understanding that a ‘search result’ is actually a composition of two interfaces:
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 from 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}`,
@ -23,8 +23,8 @@ Then we would have come to the understanding that a ‘search result’ is actua
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.
1. How will we couple the former and the latter? Obviously, these two sub-interfaces are related: the machine-readable price must match the human-readable one, for example. This will naturally lead us to the ‘formatter’ concept described in the [Chapter 16](#chapter-16).
1. How will we couple the former and the latter? Obviously, these two sub-interfaces are related: the machine-readable price must match the human-readable one, for example. This will naturally lead us to the “formatter” concept described in the [Chapter 16](#chapter-16).
2. And what is the ‘abstract representation of the search result in the UI’? Do we have other kinds of search, should the `ISearchItemViewParameters` interface be a subtype of some even more general interface, or maybe a composition of several such ones?
2. And what is the “abstract representation of the search result in the UI”? Do we have other kinds of search, should the `ISearchItemViewParameters` interface be a subtype of some even more general interface, or maybe a composition of several such ones?
Replacing specific implementations with interfaces not only allows us to answer more clearly many questions which should have popped out in the API design phase but also helps us to outline many possible API evolution vectors, which should help in avoiding API inconsistency problems in the future.

View File

@ -15,7 +15,7 @@ If fixing the error might somehow affect real customers, you have no other choic
Any software must be tested, and APIs ain't an exclusion. However, there are some subtleties there: as APIs provide formal interfaces, it's the formal interfaces that are needed to be tested. That leads to several kinds of mistakes:
1. Often the requirements like the `getEntity` function returns the value previously being set by the `setEntity` function appear to be too trivial to both developers and QA engineers to have a proper test. But it's quite possible to make a mistake there, and we have actually encountered such bugs several times.
1. Often the requirements like the `getEntity` function returns the value previously being set by the `setEntity` function appear to be too trivial to both developers and QA engineers to have a proper test. But it's quite possible to make a mistake there, and we have actually encountered such bugs several times.
2. The interface abstraction principle must be tested either. In theory, you might have considered each entity as an implementation of some interface; in practice, it might happen that you have forgotten something, and alternative implementations aren't actually possible. For testing purposes, it's highly desirable to have an alternative realization, even a provisional one.
##### Isolate the dependencies

View File

@ -2,7 +2,7 @@
There are two important statements regarding APIs viewed as products.
1. APIs are *proper products*, just like any other kind of software. You're ‘selling’ them in the same manner, and all the principles of product management are fully applicable to them. It's quite doubtful you would be able to develop APIs well unless you conducted proper market research, learned customers' needs, and studied competitors, supply, and demand.
1. APIs are *proper products*, just like any other kind of software. You're “selling” them in the same manner, and all the principles of product management are fully applicable to them. It's quite doubtful you would be able to develop APIs well unless you conducted proper market research, learned customers' needs, and studied competitors, supply, and demand.
2. Still, APIs are *quite special products*. You're selling a possibility to make some actions programmatically by writing code, and this fact puts some restrictions on product management.
@ -24,4 +24,4 @@ So it turns out that customers are at the apex of the pyramid: it is customers y
The truth, of course, lies somewhere in between. In some markets and subject areas, it is developers who make decisions (i.e. which framework to choose); in other markets and areas, it might be business owners or customers. It also depends on the competitiveness of the market: introducing a new frontend framework does not meet any resistance while developing, let's say, a new mobile operating system requires million-dollar investments into promotions and strategic partnerships.
Here and after, we will describe some ‘averaged’ situations, meaning that all three pyramid levels are important: customers choosing the product which fits their needs best, business owners seeking quality guarantees and lower development costs, as well as software engineers caring about the API capabilities and the convenience of working with it.
Here and after, we will describe some “averaged” situations, meaning that all three pyramid levels are important: customers choosing the product which fits their needs best, business owners seeking quality guarantees and lower development costs, as well as software engineers caring about the API capabilities and the convenience of working with it.

View File

@ -4,13 +4,13 @@ Before we proceed to the API product management principles description, let us d
#### Developers = end users
The easiest and the most understandable case is that of providing a service for developers, with no end users involved. First of all, we talk about different software engineering tools: APIs of programming languages, frameworks, operating systems, UI libraries, game engines, etc; in other words, general-purpose interfaces. [In our coffee API case, it means the following: we've developed a library for ordering a cup of coffee, possibly furnished with UI components, and now selling it to coffeeshop chains owners whoever willing to buy it to ease the development of their own applications.] In this case, the answer to the ‘why have an API’ question is self-evident.
The easiest and the most understandable case is that of providing a service for developers, with no end users involved. First of all, we talk about different software engineering tools: APIs of programming languages, frameworks, operating systems, UI libraries, game engines, etc; in other words, general-purpose interfaces. [In our coffee API case, it means the following: we've developed a library for ordering a cup of coffee, possibly furnished with UI components, and now selling it to coffeeshop chains owners whoever willing to buy it to ease the development of their own applications.] In this case, the answer to the “why have an API” question is self-evident.
There is also a plethora of monetizing techniques; in fact, we're just talking about monetizing software for developers.
1. The framework / library / platform might be paid per se, e.g. distributed under a commercial license. Nowadays such models are becoming less and less popular with the rise of free and open source software but are still quite common.
2. The API may be licensed under an open license with some restrictions that might be lifted by buying an extended license. It might be either functional limitations (an inability to publish the app in the app store or an incapacity to build the app in the production mode) or usage restrictions (for example, an open license might be ‘contagious’, e.g. require publishing the derived code under the same license, or using the API for some purposes might be prohibited).
2. The API may be licensed under an open license with some restrictions that might be lifted by buying an extended license. It might be either functional limitations (an inability to publish the app in the app store or an incapacity to build the app in the production mode) or usage restrictions (for example, an open license might be “contagious,” e.g. require publishing the derived code under the same license, or using the API for some purposes might be prohibited).
3. The API itself might be free, but the developer company might provide additional paid services (for example, consulting or integrating ones), or just sell the extended technical support.
@ -18,11 +18,11 @@ There is also a plethora of monetizing techniques; in fact, we're just talking a
5. Finally, the API developer company might be attracting attention to other related programming tools and hoping to increase sales by publishing the API under a free license.
Remarkably, such APIs are probably the only ‘pure’ case when developers choose the solution solely because of its clean design, elaborate documentation, thought-out use-cases, etc. There are examples of copying the API design (which is the sincerest form of flattery, as we all know!) by other companies or even enthusiastic communities — that happened, for example, with the Java language API (an alternate implementation by Google) and the C# one (the Mono project) — or just borrowing apt solutions — as it happened with the concept of selecting DOM elements with CSS selectors, initially implemented in the cssQuery project, then adopted by jQuery, and after the latter became popular, incorporated as a part of the DOM standard itself.
Remarkably, such APIs are probably the only “pure” case when developers choose the solution solely because of its clean design, elaborate documentation, thought-out use-cases, etc. There are examples of copying the API design (which is the sincerest form of flattery, as we all know!) by other companies or even enthusiastic communities — that happened, for example, with the Java language API (an alternate implementation by Google) and the C# one (the Mono project) — or just borrowing apt solutions — as it happened with the concept of selecting DOM elements with CSS selectors, initially implemented in the cssQuery project, then adopted by jQuery, and after the latter became popular, incorporated as a part of the DOM standard itself.
##### API = the main and/or the only mean of accessing the service
This case is close to the previous one as developers again, not end users, are API consumers. The difference is that the API is not a product per se, but the service exposed via the API is. The purest examples are cloud platforms APIs like Amazon AWS or Braintree API. Some operations are possible through end-user interfaces, but generally speaking, the services are useless without APIs. [In our coffee example, imagine we are an operator of ‘cloud’ coffee machines equipped with drone-powered delivery, and the API is the only mean of making an order.]
This case is close to the previous one as developers again, not end users, are API consumers. The difference is that the API is not a product per se, but the service exposed via the API is. The purest examples are cloud platforms APIs like Amazon AWS or Braintree API. Some operations are possible through end-user interfaces, but generally speaking, the services are useless without APIs. [In our coffee example, imagine we are an operator of “cloud” coffee machines equipped with drone-powered delivery, and the API is the only mean of making an order.]
Usually, customers pay for the service usage, not for the API itself, though frequently the tariffs depend on the number of API calls.
@ -63,7 +63,7 @@ If an API has neither explicit nor implicit monetization, it might still generat
The target audiences of such self-promotion might also differ:
* you might seek to increase the 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 coffee ordering widget on their websites to also pay attention to your tires catalogue];
* 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’).
* 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, e.g. the 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.
@ -73,7 +73,7 @@ If a company possesses some big data, it might be useful to provide a public API
##### Terraforming
Finally, the most altruistic approach to API product development is providing it free of charge (or as an open source and open data project) just to change the landscape. If today nobody's willing to pay for the API, we might invest in popularizing the functionality hoping to find commercial niches later (in any of the abovementioned formats) or to increase the significance and usefulness of the API integrations for end users (and therefore the readiness of the partners to pay for the API). [In the case of our coffee example, imagine a coffee machine maker that starts providing APIs for free aiming to make having an API a ‘must’ for every coffee machine vendor thus allowing for the development of commercial API-based services in the future.]
Finally, the most altruistic approach to API product development is providing it free of charge (or as an open source and open data project) just to change the landscape. If today nobody's willing to pay for the API, we might invest in popularizing the functionality hoping to find commercial niches later (in any of the abovementioned formats) or to increase the significance and usefulness of the API integrations for end users (and therefore the readiness of the partners to pay for the API). [In the case of our coffee example, imagine a coffee machine maker that starts providing APIs for free aiming to make having an API a “must” for every coffee machine vendor thus allowing for the development of commercial API-based services in the future.]
##### Gray zones
@ -81,7 +81,7 @@ One additional source of income for the API provider is the analysis of the requ
#### The API-first approach
Last several years we see the trend of providing some functionality as an API (e.g. as a product for developers) instead of developing the service for end users. This approach, dubbed ‘API-first’, reflects the growing specialization in the IT world: developing APIs becomes a separate area of expertise that business is ready to out-source instead of spending resources to develop internal APIs for the applications by in-house IT department. However, this approach is not universally accepted (yet), and you should keep in mind the factors that affect the decision of launching a service in the API-first paradigm.
Last several years we see the trend of providing some functionality as an API (e.g. as a product for developers) instead of developing the service for end users. This approach, dubbed “API-first,” reflects the growing specialization in the IT world: developing APIs becomes a separate area of expertise that business is ready to out-source instead of spending resources to develop internal APIs for the applications by in-house IT department. However, this approach is not universally accepted (yet), and you should keep in mind the factors that affect the decision of launching a service in the API-first paradigm.
1. The target market must be sufficiently heated up: there must be companies there that possess enough resources to develop services atop third-party APIs and pay for it (unless your aim is terraforming).
@ -89,4 +89,4 @@ Last several years we see the trend of providing some functionality as an API (e
3. You must really possess the expertise in API development; otherwise, there are high chances to make too many design mistakes.
Sometimes providing APIs is a method to ‘probe the ground’, e.g. to evaluate the market and decide whether it's worth having a full-scale user service there. (We rather condemn this practice as it inevitably leads to discontinuing the API or limiting its functionality, either because the market turns out to be not so profitable as expected, or because the API eventually becomes a competitor to the main service.)
Sometimes providing APIs is a method to “probe the ground,” e.g. to evaluate the market and decide whether it's worth having a full-scale user service there. (We rather condemn this practice as it inevitably leads to discontinuing the API or limiting its functionality, either because the market turns out to be not so profitable as expected, or because the API eventually becomes a competitor to the main service.)

View File

@ -1,32 +1,32 @@
### Developing a Product Vision
The above-mentioned fragmentation of the API target audience, e.g. 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 auditories, 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.
The above-mentioned fragmentation of the API target audience, e.g. 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 auditories, 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 the business would solve those problems if appropriate tools existed;
* understanding what technical solutions might exist 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 the skip 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.
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 the skip 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.
In the majority of cases, we still 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 heuristical ‘step right’ to outline future functionality for end users and one ‘step left’ to evaluate possible technical solutions.
* or, given your understanding of business owners' problems, you might make one heuristical “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: detalizing the countenance of the forthcoming will make your abstract KPIs and theoretical benefits of having an API more and more concrete.)
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: detalizing 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 code-writing inconvenience. Both will have some knowledge regarding the end users problems, but it's usually limited to the market segment the partner operates on.
If you do have an access to end users actions monitoring (see the ‘API KPIs’ chapter), then you might try to analyze the typical user behavior through these logs, and understand how users interact with the partners' applications. But you will need to make this analysis on a per-application basis, and try to clusterize the most common scenarios.
If you do have an access to end users actions monitoring (see the “API KPIs” chapter), then you might try to analyze the typical user behavior through these logs, and understand how users interact with the partners' applications. But you will need to make this analysis on a per-application basis, and try to clusterize the most common scenarios.
#### Checking product hypotheses
Apart from the general complexity of formulating the product vision, there are also tactical issues with checking product hypotheses. ‘The Holy Grail’ of product management — that is, creating a cheap (in terms of resource spent) minimal viable product (MVP) — is normally unavailable for an API product manager. The thing is that you can't easily *test* the solution even if you managed to develop an API MVP: to do so, partners are to *develop some code*, e.g. invest their money; and if the outcome of the experiment is negative (e.g. the further development looks unpromising), this money will be wasted. Of course, partners will be a little bit skeptical towards such proposals. Thus a ‘cheap’ MVP should include either the compensation for partners' expenses or the budget to develop a reference implementation (e.g. a complementary application must be developed alongside the API MVP).
Apart from the general complexity of formulating the product vision, there are also tactical issues with checking product hypotheses. “The Holy Grail” of product management — that is, creating a cheap (in terms of resource spent) minimal viable product (MVP) — is normally unavailable for an API product manager. The thing is that you can't easily *test* the solution even if you managed to develop an API MVP: to do so, partners are to *develop some code*, e.g. invest their money; and if the outcome of the experiment is negative (e.g. the further development looks unpromising), this money will be wasted. Of course, partners will be a little bit skeptical towards such proposals. Thus a “cheap” MVP should include either the compensation for partners' expenses or the budget to develop a reference implementation (e.g. a complementary application must be developed alongside the API MVP).
You might partially solve the problem by making some third-party company release the MVP (for example, in a form of an open source module published in some developer's personal repository) but then you will struggle with hypothesis validation issues as such modules might easily go unnoticed.
Another option for checking conjectures is recruiting an API provider company's services developers if they exist. Internal customers are usually much more loyal towards spending some effort to check a hypothesis, and it's much easier to negotiate MVP curtailing or freezing with them. The problem is that you can check only those ideas that are relevant to internal services needs.
Generally speaking, the ‘eat your own dogfood’ concept applied to APIs means that the product team should have their own test applications (i.e. ‘pet project’) on top of the APIs. Given the complexity of developing such applications, it makes sense to encourage having them, e.g. giving free API quotas to team members and providing sufficient free computational resources.
Generally speaking, the “eat your own dogfood” concept applied to APIs means that the product team should have their own test applications (i.e. “pet project”) on top of the APIs. Given the complexity of developing such applications, it makes sense to encourage having them, e.g. giving free API quotas to team members and providing sufficient free computational resources.
Such pet projects are also valuable because of the unique experience they allow to gain: everyone might try a new role. Developers will learn product managers' typical problems: it's not enough to write a fine code, you also need to know your customer, understand their demands, formulate an attractive concept, and communicate it. In their turn, product managers will get some understanding of how exactly easy or hard is to render their product vision into life, and what problems the implementation will bring. Finally, both will benefit from taking a fresh look at the API documentation and putting themselves in the shoes of a developer who had heard about the API product for the first time and is now struggling with grasping the basics.

View File

@ -12,12 +12,12 @@ Let's start with developers. The specifics of software engineers as an auditory
* 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 (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 (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.
@ -30,7 +30,7 @@ Developers do like sharing the experience, and will probably be eager to do it
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.) An evangelist is thus making 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.)
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.) An evangelist is thus making 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
@ -52,4 +52,4 @@ It will be more correct if we say that you're actually working for two main audi
* professional developers who possess a 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.
This fact greatly affects everything we had discussed previously (except for, maybe, open-sourcing, as amateur developers pay little attention to it). Your pitches, webinars, lectures, etc., must somehow fit both professional and semi-professional auditories. There is a popular opinion that you actually can't satisfy both: the former seeks extensive customization capabilities (as they usually work in big IT companies that have a specific mindset regarding those integrations) while the latter just needs the gentlest possible learning curve. We would rather disagree with that, the reasons to be discussed in the ‘API Services Range’ chapter.
This fact greatly affects everything we had discussed previously (except for, maybe, open-sourcing, as amateur developers pay little attention to it). Your pitches, webinars, lectures, etc., must somehow fit both professional and semi-professional auditories. There is a popular opinion that you actually can't satisfy both: the former seeks extensive customization capabilities (as they usually work in big IT companies that have a specific mindset regarding those integrations) while the latter just needs the gentlest possible learning curve. We would rather disagree with that, the reasons to be discussed in the “API Services Range” chapter.

View File

@ -4,7 +4,7 @@ The basics of interacting with business partners are to some extent paradoxicall
* on one side, partners are much more loyal and sometimes even enthusiastic regarding opportunities you offer (especially free ones);
* on another side, 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 having an API (as a concept) compared to having a service for end users.
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.
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 is from information technologies, the more enthusiastic its representatives are about your API features, and the less the chance is that this enthusiasm will be converted into a real integration. The one thing that should help the case is extensive work with the community (see the corresponding chapter) that will result in establishing a circle of freelances and outsourcers eager to help non-IT businesses with integrations. You might help develop this market by the creation of educational courses and issuing certificates proving the bearer's skills of working with your API (or some broader layer of technology).

View File

@ -8,9 +8,9 @@ Usually, any functionality available through an API might be split into independ
Different companies employ different approaches to determining the granularity of API services, 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, e.g. some isolated subset of the API provides 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 backwards compatibility and introduce new major versions only when it's absolutely necessary; otherwise, maintaining a matrix which API No. 1 version is compatible with which API No. 2 version will soon become a catastrophe);
* 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 backwards compatibility and introduce new major versions only when it's absolutely necessary; otherwise, maintaining a matrix which API No. 1 version is compatible with which API No. 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.
* 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 programmer's life easier.
@ -25,7 +25,7 @@ The most important conclusion here is that the only way to cover the needs of al
1. The most advanced level is that of physical APIs and the abstractions on top of them. [In our coffee example, the collection of entities describing working with APIs of physical coffee machines, see [Chapter 9](#chapter-9) and [Chapter 17](#chapter-17).]
2. The basic level of working with product entities via formal interfaces. [In our study example, that will be HTTP API for making orders.]
3. Working with product entities might be simplified if SDKs are provided for some popular platforms that tailor API concepts according to the paradigms of those platforms (for those developers who are proficient with specific platforms only that will save a lot of effort on dealing with formal protocols and interfaces).
4. The next simplification step is providing services for code generation. In this service, developers choose one of the pre-built integration templates, customize some options, and got a ready-to-use piece of code that might be simply copy-pasted into the application code (and might be additionally customized by adding some level 1-3 code). This approach is sometimes called ‘point-and-click programming’. [In the case of our coffee API, an example of such a service might have a form or screen editor for a developer to place UI elements and get the working application code.]
4. The next simplification step is providing services for code generation. In this service, developers choose one of the pre-built integration templates, customize some options, and got a ready-to-use piece of code that might be simply copy-pasted into the application code (and might be additionally customized by adding some level 1-3 code). This approach is sometimes called “point-and-click programming.” [In the case of our coffee API, an example of such a service might have a form or screen editor for a developer to place UI elements and get the working application code.]
5. Finally, this approach might be simplified even further if the service generates not code but a ready-to-use component / widget / frame and a one-liner to integrate it. [For example, if we allow embedding an iframe that handles the entire coffee ordering process right on the partner's website, or describes the rules of forming the image URL that will show the most relevant offer to an end user if embedded in the partner's app.]
Ultimately, we will end up with a concept of meta-API, e.g. those high-level components will have an API of their own built on top of the basic API.
@ -37,6 +37,6 @@ The most important advantage of having a range of APIs is not only about adaptin
4. Code generation makes it possible to manipulate the desired form of integrations. For example, if our KPI is a number of searches performed through the API, we might alter the generated code so it will show the search panel in the most convenient position in the app; as partners using code-generation services rarely make any changes in the resulting code, this will help you in reaching the goal.
5. Finally, ready-to-use components and widgets are under your full control, and you might experiment with functionality exposed through them in partners' applications just like if it was your own service. (However, it doesn't automatically mean that you might draw some profits from having this control; for example, if you're allowing inserting pictures by their direct URL, your control over this integration is rather negligible, so it's generally better to provide those kinds of integration that allow having more control over the functionality in partners' apps.)
**NB**. While developing a ‘vertical’ range of APIs, following the principles stated in the [Chapter 14 ‘On the Iceberg's Waterline’](#chapter-14) is crucial. You might manipulate widget content and behavior if, and only if, developers can't ‘escape the sandbox’, e.g. have direct access to low-level objects encapsulated within the widget.
**NB**. While developing a “vertical” range of APIs, following the principles stated in the [Chapter 14 “On the Iceberg's Waterline”](#chapter-14) is crucial. You might manipulate widget content and behavior if, and only if, developers can't “escape the sandbox,” e.g. have direct access to low-level objects encapsulated within the widget.
In general, you should aim to have each partner service using the API service that maximizes your profit as an API vendor. Where the partner doesn't try to make some unique experience and needs just a typical solution, you would benefit from making them use widgets, which are under your full control and thus ease the API version fragmentation problem and allow for experimenting in order to reach your KPIs. Where the partner possesses some unique expertise in the subject area and develops a unique service on top of your API, you would benefit from allowing full freedom in customizing the integration, so they might cover specific market niches and enjoy the advantage of offering more flexibility compared to using other APIs.

View File

@ -35,13 +35,13 @@ Let us summarize the paragraph:
#### 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 point out 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 data access or a content licensing service.
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 point out 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 data access or a content licensing service.
Still, let us re-iterate once more: any problems with your API are automatically multiplied by the number of partners you have, especially if the API is vital for them, e.g. the API outage makes the main functionality of their services unavailable. (And actually, because of the above-mentioned reasons, the average quality of integrations implies that partners' services will suffer even if the API is not formally speaking critical for them, but because developers use it excessively and do not bother with proper error handling.)
It is important to mention that predicting the workload for the API service is rather complicated. Sub-optimal API usage, e.g. initializing the API in those application and website parts where it's not actually needed, might lead to a colossal increase in the number of requests after changing a single line of partner's code. The safety margin for an API service must be much higher than for a regular service for end users — it must survive the situation of the largest partner suddenly starting querying the API on every page and every application screen. (If the partner is already doing that, then the API must survive doubling the load: imagine the partner accidentally starts initializing the API twice on each page / screen.)
Another extremely important hygienic minimum is the informational security of the API service. In the worst-case scenario, namely, if an API service vulnerability allows for exploiting partner applications, one security loophole will in fact be exposed *in every partner application*. Needless to say that the cost of such a mistake might be overwhelmingly colossal, even if the API itself is rather trivial and has no access to sensitive data (especially if we talk about webpages where no ‘sandbox’ for third-party scripts exists, and any piece of code might let's say track the data entered in forms). API services must provide the maximum protection level (for example, choose cryptographical protocols with a certain overhead) and promptly react to any messages regarding possible vulnerabilities.
Another extremely important hygienic minimum is the informational security of the API service. In the worst-case scenario, namely, if an API service vulnerability allows for exploiting partner applications, one security loophole will in fact be exposed *in every partner application*. Needless to say that the cost of such a mistake might be overwhelmingly colossal, even if the API itself is rather trivial and has no access to sensitive data (especially if we talk about webpages where no “sandbox” for third-party scripts exists, and any piece of code might let's say track the data entered in forms). API services must provide the maximum protection level (for example, choose cryptographical protocols with a certain overhead) and promptly react to any messages regarding possible vulnerabilities.
#### Comparing to Competitors

View File

@ -10,7 +10,7 @@ In most cases, you need to have both of them identified (in a technical sense: d
* 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.
**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:

View File

@ -13,19 +13,19 @@ Generally speaking, there are two approaches we might take, the static one and t
*Statically* we monitor suspicions activity surges, as described in the previous chapter, marking an unusually high density of requests coming from specific networks or Referers (actually, *any* piece of information suits if it divides users into more or less independent groups: for example, OS version or system language if you can gather those).
*Behavioral* analysis means we're examining the history of requests made by a specific user, searching for non-typical patterns, such as ‘unhuman’ order of traversing endpoints or too small pauses between requests.
*Behavioral* analysis means we're examining the history of requests made by a specific user, searching for non-typical patterns, such as “unhuman” order of traversing endpoints or too small pauses between requests.
**Importantly**, when we talk about ‘user’, we will have to make a second analytical contour to work with IP addresses, as malefactors aren't obliged to preserve cookies or other identification tokens, or will keep a pool of such tokens to impede their exposure.
**Importantly**, when we talk about “user,” we will have to make a second analytical contour to work with IP addresses, as malefactors aren't obliged to preserve cookies or other identification tokens, or will keep a pool of such tokens to impede their exposure.
##### Requesting an additional authentication factor
As both static and behavioral analyses are heuristic, it's highly desirable to not make decisions based solely on their outcome, but rather ask the suspicious users to additionally prove they're making legitimate requests. If such a mechanism is in place, the quality of an anti-fraud system will be dramatically improved, as it allows for increasing system sensitivity and enabling pro-active defense, e.g. asking users to pass the tests in advance.
In the case of services for end users, the main method of acquiring the second factor is redirecting to a captcha page. In the case of the API it might be problematic, especially if you initially neglected the [‘Stipulate restrictions’](#chapter-11-paragraph-19) advice. In many cases, you will have to impose this responsibility on partners (e.g. it will be partners who show captchas and identify users based on the signals received from the API endpoints). This will, of course, significantly impair the convenience of working with the API.
In the case of services for end users, the main method of acquiring the second factor is redirecting to a captcha page. In the case of the API it might be problematic, especially if you initially neglected the [“Stipulate restrictions”](#chapter-11-paragraph-19) advice. In many cases, you will have to impose this responsibility on partners (e.g. it will be partners who show captchas and identify users based on the signals received from the API endpoints). This will, of course, significantly impair the convenience of working with the API.
**NB**. Instead of captcha, there might be any other actions introducing additional authentication factors. It might be the phone number confirmation or the second step of the 3D-Secure protocol. The important part is that requesting an additional authentication step must be stipulated in the program interface, as it can't be added later in a backwards-compatible manner.
Other popular mechanics of identifying robots include offering a bait (‘honeypot’) or employing the execution environment checks (starting from rather trivial like executing JavaScript on the webpage and ending with sophisticated techniques of checking application integrity).
Other popular mechanics of identifying robots include offering a bait (“honeypot”) or employing the execution environment checks (starting from rather trivial like executing JavaScript on the webpage and ending with sophisticated techniques of checking application integrity).
##### Restricting access
@ -47,7 +47,7 @@ In most cases, you're not fighting fraud — you're actually increasing the cost
An opinion exists, which the author of this book shares, that engaging into this sword-against-shield confrontation must be carefully thought out, and advanced technical solutions are to be enabled only if you are one hundred percent sure it is worth it (e.g. if they steal real money or data). By introducing elaborate algorithms, you rather conduct an evolutional selection of the smartest and most cunning cybercriminals, counteracting whom will be way harder than those who just naively call API endpoints with `curl`. What is even more important, in the final phase — e.g. when filing the complaint to authorities — you will have to prove the alleged ToS violation, and doing so against an advanced fraudster will be problematic. So it's rather better to have all the malefactors monitored (and regularly complained against), and escalate the situation (e.g. enable the technical protection and start legal actions) only if the threat passes a certain threshold. That also implies that you must have all the tools ready, and just keep them below fraudsters' radars.
Out of the author of this book's experience, the mind games with malefactors, when you respond to any improvement of their script with the smallest possible effort that is enough to break it, might continue indefinitely. This strategy, e.g. making fraudsters guess which traits were used to ban them this time (instead of unleashing the whole heavy artillery potential), annoys amateur ‘hackers’ greatly as they lack hard engineering skills and just give up eventually.
Out of the author of this book's experience, the mind games with malefactors, when you respond to any improvement of their script with the smallest possible effort that is enough to break it, might continue indefinitely. This strategy, e.g. making fraudsters guess which traits were used to ban them this time (instead of unleashing the whole heavy artillery potential), annoys amateur “hackers” greatly as they lack hard engineering skills and just give up eventually.
#### Dealing with stolen keys

View File

@ -7,11 +7,11 @@ Before we start describing documentation types and formats, we should stress one
1. First, you need to determine whether this service covers your needs in general (as quickly as possible);
2. If it does, you look for specific functionality to resolve your specific case.
In fact, newcomers (e.g. those developers who are not familiar with the API) usually want just one thing: to assemble the code that solves their problem out of existing code samples and never return to this issue again. Sounds not exactly reassuringly, given the amount of work invested into the API and its documentation development, but that's what the reality looks like. Also, that's the root cause of developers' dissatisfaction with the docs: it's literally impossible to have articles covering exactly that problem the developer comes with being detailed exactly to the extent the developer knows the API concepts. In addition, non-newcomers (e.g. those developers who have already learned the basics concepts and are now trying to solve some advanced problems) do not need these ‘mixed examples’ articles as they look for some deeper understanding.
In fact, newcomers (e.g. those developers who are not familiar with the API) usually want just one thing: to assemble the code that solves their problem out of existing code samples and never return to this issue again. Sounds not exactly reassuringly, given the amount of work invested into the API and its documentation development, but that's what the reality looks like. Also, that's the root cause of developers' dissatisfaction with the docs: it's literally impossible to have articles covering exactly that problem the developer comes with being detailed exactly to the extent the developer knows the API concepts. In addition, non-newcomers (e.g. those developers who have already learned the basics concepts and are now trying to solve some advanced problems) do not need these “mixed examples” articles as they look for some deeper understanding.
#### Introductory notes
Documentation frequently suffers from being excessively clerical; it's being written using formal terminology (which often requires reading the glossary before the actual docs) and being unreasonably inflated. So instead of a two-word answer to the user's question a couple of paragraphs is conceived — a practice we strongly disapprove of. The perfect documentation must be simple and laconic, and all the terms must be either explained in the text or given a reference to such an explanation. However, ‘simple’ doesn't mean ‘illiterate’: remember, the documentation is the face of your product, so grammar errors and improper usage of terms are unacceptable.
Documentation frequently suffers from being excessively clerical; it's being written using formal terminology (which often requires reading the glossary before the actual docs) and being unreasonably inflated. So instead of a two-word answer to the user's question a couple of paragraphs is conceived — a practice we strongly disapprove of. The perfect documentation must be simple and laconic, and all the terms must be either explained in the text or given a reference to such an explanation. However, “simple” doesn't mean “illiterate”: remember, the documentation is the face of your product, so grammar errors and improper usage of terms are unacceptable.
Also, keep in mind that documentation will be used for searching as well, so every page should contain all the keywords required to be properly ranked by search engines. This requirement somehow contradicts the simple-and-laconic principle; that's the way.
@ -40,7 +40,7 @@ Ideally, examples should be linked to all other kinds of documentation, i.e. the
##### Sandboxes
Code samples will be much more useful to developers if they are ‘live’, e.g. provided as live 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:
Code samples will be much more useful to developers if they are “live,” e.g. provided as live 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.
@ -48,29 +48,29 @@ Code samples will be much more useful to developers if they are ‘live’, e.g.
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 a ‘book’ that you write to explain to the reader how to work with your API. So, a proper tutorial must follow book-writing patterns, e.g. explain the concepts coherently and consecutively chapter after chapter. Also, a tutorial must provide:
A tutorial is a sort of a “book” that you write to explain to the reader how to work with your API. So, a proper tutorial must follow book-writing patterns, e.g. 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 some cartographical API must explain trivia regarding geographical coordinates and working with them;
* proper API usage scenarios, e.g. the ‘happy paths’;
* proper API usage scenarios, e.g. the “happy paths”;
* proper reactions to program errors that could happen;
* detailed studies on advanced API functionality (with detailed examples).
As usual, 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.
Usually, tutorials contain a ‘Quick Start’ (‘Hello, world!’) section: the smallest possible code sample that would allow developers to build a small app atop of the API. ‘Quick starts’ aim to cover two needs:
Usually, tutorials contain a “Quick Start” (“Hello, world!”) section: the smallest possible code sample that would allow developers to build a small app atop of 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.
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.
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.
##### Frequently asked questions and a knowledge base
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:
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.
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 generally 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, e.g. a set of ‘real-life’ questions and answers.
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, e.g. a set of “real-life” questions and answers.
##### Offline documentation

View File

@ -6,8 +6,8 @@ However, in many cases having a test version is not enough — like in our coffe
A direct solution to this problem is providing a full set of testing APIs and administrative interfaces. It means that developers will need 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:
* end user application developers 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.
* 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.
@ -17,11 +17,11 @@ The first option is providing a meta-API to the testing environment itself. Inst
Ideally, you should provide helper methods for any actions that are conducted by people in production environment. It makes sense to ship this meta-API complete with ready-to-use scripts or request collections that show the correct API call orders for standard scenarios.
The disadvantage of this approach is that client developers still need to know how the ‘flip side’ of the system works, though in simplified terms.
The disadvantage of this approach is that client developers still need to know how the “flip side” of the system works, though in simplified terms.
##### The simulator of pre-defined scenarios
The alternative to providing the testing environment API is simulating the working scenarios. In this case, the testing environment takes control over ‘underwater’ parts of the system and ‘plays’ all external agents' actions. In our coffee example, that means that, after the order is submitted, the system will simulate all the preparation steps and then the delivery of the beverage to the customer.
The alternative to providing the testing environment API is simulating the working scenarios. In this case, the testing environment takes control over “underwater” parts of the system and “plays” all external agents' actions. In our coffee example, that means that, after the order is submitted, the system will simulate all the preparation steps and then the delivery of the beverage to the customer.
The advantage of this approach is that it demonstrates vividly how the system works according to the API vendor design plans, e.g. in which sequence the events are generated, and which stages the order passes through. It also reduces the chance of making mistakes in testing scripts, as the API vendor guarantees the actions will be executed in the correct order with the right parameters.
@ -31,4 +31,4 @@ The main disadvantage is the necessity to create a separate scenario for each un
Your final goal in implementing testing APIs, regardless of which option you choose, is allowing partners to automate the QA process for their products. The testing environment should be developed with this purpose in mind; for example, if an end user might be brought to a 3-D Secure page to pay for the order, the testing environment API must provide some way of simulating the successful (or not) passing of this step. Also, in both variants, it's possible (and desirable) to allow running the scenarios in a fast-forward manner that will allow making auto-testing much faster than manual testing.
Of course, not every partner will be able to employ this possibility (which also means that ‘manual’ way of testing usage scenarios must also be supported alongside the programmatical one) simply because not every business might afford to hire a QA automation engineer. Nevertheless, the ability to write such auto-tests is your API's huge competitive advantage from a technically advanced partner's point of view.
Of course, not every partner will be able to employ this possibility (which also means that “manual” way of testing usage scenarios must also be supported alongside the programmatical one) simply because not every business might afford to hire a QA automation engineer. Nevertheless, the ability to write such auto-tests is your API's huge competitive advantage from a technically advanced partner's point of view.

View File

@ -8,7 +8,7 @@ Ideally, the API once published should live eternally; but as we all are reasona
The author of this book formulates the rule of issuing new major API versions like this: the period of time after which partners will need to rewrite the code should coincide with the application lifespan in the subject area. Any program will once become obsolete and will be rewritten; and if during this re-developing partners need to also switch to a newer API version, it will be met with some degree of understanding. Of course, in different subject areas, this timespan differs, depending on the evolution rate of the underlying platform.
Apart from updating *major* versions, sooner or later you will face issues with accessing some outdated *minor* versions as well. As we mentioned in the ‘On the Waterline of the Iceberg’ chapter, even fixing bugs might eventually lead to breaking some integrations, and that naturally leads us to the necessity of keeping older *minor* versions of the API until the partner resolves the problem.
Apart from updating *major* versions, sooner or later you will face issues with accessing some outdated *minor* versions as well. As we mentioned in the “On the Waterline of the Iceberg” chapter, even fixing bugs might eventually lead to breaking some integrations, and that naturally leads us to the necessity of keeping older *minor* versions of the API until the partner resolves the problem.
In this aspect, integrating with large companies that have a dedicated software engineering department differs dramatically from providing a solution to individual amateur programmers: from one side, the former are much more likely to find undocumented features and unfixed bugs in your code; on the other side, because of the internal bureaucracy, fixing the related issues might easily take months, save not years. The common recommendation there is to maintain old minor API versions for a period of time long enough for the most dilatory partner to switch no the newest version.

View File

@ -2,11 +2,11 @@
The situation with HTTP status codes demonstrated a disastrous colliding of a well-meaning specification design with a ruthless reality as nothing before. This collision actually comes from three sides.
As we discussed in the [Chapter 10](https://twirl.github.io/The-API-Book/docs/API.en.html#chapter-10), one goal of making errors semantic is to help clients understand, what caused an error. HTTP errors, outlined in the corresponding RFCs (most recently in the [RFC 7231](https://tools.ietf.org/html/rfc7231#section-6)), are specifically designed bearing this purpose in mind. Furthermore, the REST architectural constraints, as [defined by Fielding](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm), imply that not only end user agents should understand error code, but also every network proxy between a client and a server (the layered’ architecture principle). And, in accordance to Fielding's writings, HTTP status code nomenclature does extensively describe virtually every situation which could happen with your HTTP request: wrong `Accept-*` headers value, `Content-Length` is absent, HTTP method is unsupported, URI too long, etc.
As we discussed in the [Chapter 10](https://twirl.github.io/The-API-Book/docs/API.en.html#chapter-10), one goal of making errors semantic is to help clients understand, what caused an error. HTTP errors, outlined in the corresponding RFCs (most recently in the [RFC 7231](https://tools.ietf.org/html/rfc7231#section-6)), are specifically designed bearing this purpose in mind. Furthermore, the REST architectural constraints, as [defined by Fielding](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm), imply that not only end user agents should understand error code, but also every network proxy between a client and a server (the layered’ architecture principle). And, in accordance to Fielding's writings, HTTP status code nomenclature does extensively describe virtually every situation which could happen with your HTTP request: wrong `Accept-*` headers value, `Content-Length` is absent, HTTP method is unsupported, URI too long, etc.
What the RFC fails to describe is what to do with the error. As we discussed, errors could be resolvable or not. If the error is unresolvable, all this status codes and headers stuff is simply irrelevant to clients, even more so to interim proxies. In fact, three error codes are enough:
* `400` to denote persistent situation (error couldn't be resolved by just repeating the request);
* `404` to denote uncertainty’ cases (the request could be repeated — possibly with different outcomes);
* `404` to denote uncertainty’ cases (the request could be repeated — possibly with different outcomes);
* `500` to denote server-side problems, with `Retry-After` header to indicate the desirable retry period.
**Aside note:** mark a design flaw here. All `4xx` status codes are by default not cacheable, except for `404`, `405`, `410` and `414`, which are cacheable. We presume that editors of the spec did this with the best intentions, but the number of people who knows this nuance is probably quite close to the number of the spec editors. As a result, there are lots of cases (the author of this book had to deal with one) when `404`s were returned erroneously and cached on clients, thus prolonging the outage for an indefinite time.
@ -27,7 +27,7 @@ As for *resolvable* errors, having status codes nomenclature partially helps. So
So we quite naturally are moving to the idea of denoting error details with headers and/or response bodies, not trying to invent a specific error code to each situation. It's quite obvious that we can't design a new error code for every possible parameter missing in case of `400` error, for example.
**Aside note**: the spec authors understood this too, adding the following sentence: ‘The response message will usually contain a representation that explains the status. We couldn't agree more, but this sentence not only renders the entire spec section redundant (why use status codes in the first place?), but also contradicts to the REST paradigm: other agents in the layered system couldn't understand what the response message explains, thus making the error appear opaque to them.
**Aside note**: the spec authors understood this too, adding the following sentence: ‘The response message will usually contain a representation that explains the status. We couldn't agree more, but this sentence not only renders the entire spec section redundant (why use status codes in the first place?), but also contradicts to the REST paradigm: other agents in the layered system couldn't understand what the response message explains, thus making the error appear opaque to them.
The conclusion seems to be: use status codes just to indicate a general error class, expressed in the HTTP protocol terms, and fill the response body with details. But here the third collision occurs: the implementation practice. From the very beginning of the Web, frameworks and server software started relying on status codes for logging and monitoring purposes. I think I wouldn't exaggerate gravely if I said that there were literally no platform which natively supported building charts and graphs using custom semantic data in the responses, not status codes. One severe implication is that developers started inventing new codes to have their monitoring work correctly, cutting off insignificant errors and escalating vital ones.
@ -50,7 +50,7 @@ Actually, there are three different approaches to solve this situation.
* `500 Internal Server Error` if the request can't reach the server.
You may employ the `400 Bad Request` also, to denote client errors; it slightly complicates the setup, but allows for using some interim software like API gateways.
* ‘Run with scissors, using common practices, just cautiously, avoiding violating HTTP semantics. Use HTTP status codes to separate graphs (sometimes using exotic codes). Describe errors semantically and make sure clients don't try to detect anything valuable from the status codes.
* ‘Run with scissors, using common practices, just cautiously, avoiding violating HTTP semantics. Use HTTP status codes to separate graphs (sometimes using exotic codes). Describe errors semantically and make sure clients don't try to detect anything valuable from the status codes.
**NB**: some industrial-grade platforms manage to do both, i.e. combine a pure RPC-style approach with extensively employing various HTTP status codes to indicate a subset of problems (`403`s and `429`s for instance, which are purely business logic-bound, having nothing to do with the HTTP itself). Though in a practical sense this approach seems to work, it's very hard to to tell which problems they face in modern smart-proxy rich environments, not mentioning aesthetic impressions.
* Try organizing the mess. Including, but not limited to:

View File

@ -4,7 +4,7 @@
No other technology in the IT history generated as many fierce debates as REST did. The most remarkable thing is that disputants usually demonstrate totally no understanding of the subject under discussion.
Let's start with the very beginning. In 2000 Roy Fielding, one of the HTTP and URI specs authors, defended his doctoral dissertation on ‘Architectural Styles and the Design of Network-based Software Architectures. Fifth chapter of this dissertation is ‘Representational State Transfer (REST). It might be found [there](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm).
Let's start with the very beginning. In 2000 Roy Fielding, one of the HTTP and URI specs authors, defended his doctoral dissertation on ‘Architectural Styles and the Design of Network-based Software Architectures. Fifth chapter of this dissertation is ‘Representational State Transfer (REST). It might be found [there](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm).
As anyone may ascertain upon reading this chapter, it holds quite an abstract overview of distributed network architecture, which is bound to neither HTTP nor URI. Furthermore, it is not at all about designing APIs. In this chapter Fielding methodically enumerates the restrictions which distributed systems software engineer has to deal with. Here they are:
@ -15,7 +15,7 @@ As anyone may ascertain upon reading this chapter, it holds quite an abstract ov
* systems are layered, e.g. server might be but a proxy to other servers;
* client's functionality might be extended by server providing code on demand.
That's all. Essentially REST is defined like this. In the rest of the chapter Fielding elaborates over different system implementation aspects, but all of them are just as well abstract. Literally: ‘the key abstraction of information in REST is a resource; any information that can be named can be a resource.
That's all. Essentially REST is defined like this. In the rest of the chapter Fielding elaborates over different system implementation aspects, but all of them are just as well abstract. Literally: ‘the key abstraction of information in REST is a resource; any information that can be named can be a resource.
The key conclusion from the Fielding's REST definition is actually this: *any network-based software in the world complies to REST principles*, except on very rare occasions.
@ -141,7 +141,7 @@ Let us also note that numerous ‘REST API how-to’-s which could be found on t
4. ‘Don't nest the resources’ — this rule just reflects the fact that entities' relations tend to evolve over time, and strict hierarchies eventually become not-so-strict.
5. ‘Use plural form for resources, ‘enforce trailing slash’ and related pieces of advice, which are just about the code style, not REST.
5. ‘Use plural form for resources, ‘enforce trailing slash’ and related pieces of advice, which are just about the code style, not REST.
In the end, let us dare to state four rules that would *actually* help in designing a REST API:

View File

@ -27,6 +27,6 @@
Другой вариант проверки гипотез — это собственный сервис (или сервисы) компании-разработчика API, если такие есть. Внутренние заказчики обычно более лояльно относятся к трате ресурсов на проверку гипотез, и с ними легче договориться о сворачивании или замораживании MVP. Однако таким образом можно проверить далеко не всякую функциональность — а только то, на которую имеется хоть в каком-то приближении внутренний заказ.
Вообще, концепция ‘eat your own dogfood’ применительно к API означает, что у команды продукта есть какие-то свои тестовые приложения (т.н. ‘pet project’-ы) поверх API. Учитывая трудоёмкость разработки подобных приложений, имеет смысл поощрять их наличие, например, предоставлением бесплатных квот на API и вычислительные ресурсы членам команды.
Вообще, концепция “eat your own dogfood” применительно к API означает, что у команды продукта есть какие-то свои тестовые приложения (т.н. “pet project”-ы) поверх API. Учитывая трудоёмкость разработки подобных приложений, имеет смысл поощрять их наличие, например, предоставлением бесплатных квот на API и вычислительные ресурсы членам команды.
Подобные pet project-ы также дают уникальный опыт: каждый может попробовать себя в новой роли. Разработчик узнает о типичных проблемах менеджера продукта: недостаточно написать приложение хорошо, нужно ещё и изучить своего потребителя, понять его потребности, сформулировать привлекательное предложение и донести его. Менеджеры продукта же, соответственно, получат какое-то представление о том, насколько технически просто или сложно воплотить их продуктовое видение в жизнь, и какие проблемы возникнут при реализации. Наконец, и тем, и другим будет исключительно полезно взглянуть со стороны документацию API, на user story разработчика, который впервые услышал о продукте API и пытается в нём разобраться.

View File

@ -17,7 +17,7 @@
* идеи Open Source и бесплатного ПО распространены среди разработчиков достаточно широко; попытки в том или ином виде зарабатывать на вещах, которые должны быть бесплатными и/или открытыми (например, путём лицензирования интеллектуальной собственности на интерфейсы), будут встречать сопротивление, причём представление об этих «должны быть» существенно варьируется.
В силу этих особенностей аудитории (в первую очередь — малой роли инфлюэнсеров и критического отношения к рекламным заявлениям) доносить информацию до разработчиков приходится через специфические каналы:
* коллективные блоги (такие, например, как субреддит ‘r/programming’ или dev.to);
* коллективные блоги (такие, например, как субреддит “r/programming” или dev.to);
* сайты вопросов и ответов (StackOverflow, Experts Exchange);
* обучающие сервисы (CodeAcademy, Udemy и другие);
* технические конференции и вебинары.

View File

@ -55,7 +55,7 @@
Как правило, руководство представляет собой общий блок (основные термины и понятия, используемые обозначения) и набор блоков по каждому роду функциональности, предоставляемой через API.
Часто в составе руководства выделяется т.н. ‘quick start’ (‘Hello, world!’): максимально короткий пример, позволяющий новичку собрать хотя бы какое-то минимальное приложение поверх API. Целей его существования две:
Часто в составе руководства выделяется т.н. “quick start” (“Hello, world!”): максимально короткий пример, позволяющий новичку собрать хотя бы какое-то минимальное приложение поверх API. Целей его существования две:
* стать точкой входа по умолчанию, максимально понятным и полезным текстом для тех, кто впервые услышал о вашем API;
* вовлечь разработчиков, дать им «пощупать» сервис на живом примере.

View File

@ -49,7 +49,7 @@ GET /v1/orders/created-history⮠
#### Доставка сообщений backend-to-backend
В отличие от клиентских приложений, серверные API практически безрезультативно используют единственный подход — отдельный канал связи для обратных вызовов. При интеграции партнёр указывает URL своего собственного сервера обработки сообщений, и сервер API вызывает этот эндпойнт (также называемый ‘webhook’) для оповещения о произошедшем событии. Хотя long polling, Web Sockets и HTTP/2 Push тоже вполне применимы для backend-2-backend взаимодействия, мы сходу затрудняемся назвать примеры популярных API, которые использовали бы эти технологии. Главными причинами такого положения дел нам видятся:
В отличие от клиентских приложений, серверные API практически безрезультативно используют единственный подход — отдельный канал связи для обратных вызовов. При интеграции партнёр указывает URL своего собственного сервера обработки сообщений, и сервер API вызывает этот эндпойнт (также называемый “webhook”) для оповещения о произошедшем событии. Хотя long polling, Web Sockets и HTTP/2 Push тоже вполне применимы для backend-2-backend взаимодействия, мы сходу затрудняемся назвать примеры популярных API, которые использовали бы эти технологии. Главными причинами такого положения дел нам видятся:
* бо́льшая критичность корректной и своевременной обработки событий, и отсюда повышенные требования к гарантиям их доставки;
* возможность покрыть такое взаимодействие строгой спецификацией.

View File

@ -30,7 +30,7 @@
Таким образом, мы вполне естественным образом приходим к идее отдавать детальное описание ошибки в заголовках и/или теле ответа, не пытаясь изобрести новый код для каждой ситуации — абсолютно очевидно, что нельзя задизайнить по ошибке на каждый потенциально неправильный параметр вместо единой `400`-ки, например.
**Замечание**: авторы спецификации тоже это понимали, и добавили следующую фразу: ‘The response message will usually contain a representation that explains the status. Мы с ними, конечно, полностью согласны, но не можем не отметить, что эта фраза не только делает кусок спецификации бесполезным (а зачем нужны коды-то тогда?), но и противоречит парадигме REST: другие агенты в многоуровневой системе не могут понять, что же там «объясняет» представление ошибки, и сама ошибка становится для них непрозрачной.
**Замечание**: авторы спецификации тоже это понимали, и добавили следующую фразу: ‘The response message will usually contain a representation that explains the status. Мы с ними, конечно, полностью согласны, но не можем не отметить, что эта фраза не только делает кусок спецификации бесполезным (а зачем нужны коды-то тогда?), но и противоречит парадигме REST: другие агенты в многоуровневой системе не могут понять, что же там «объясняет» представление ошибки, и сама ошибка становится для них непрозрачной.
Казалось бы, мы пришли к логичному выводу: используйте статус-коды для индикации «класса» ошибки в терминах протокола HTTP, а детали положите в ответ. Но вот тут теория повторно на всех парах напарывается на практику. С самого появления Web все фреймворки и серверное ПО полагаются на статус-коды для логирования и построения мониторингов. Я не думаю, что сильно совру, если скажу, что буквально не существует платформы, которая из коробки умеет строить графики по семантическим данным в ответе ошибки, а не по статус-кодам. И отсюда автоматически следует дальнейшее усугубление проблемы: чтобы отсечь в своих мониторингах незначимые ошибки и эскалировать значимые, разработчики начали попросту придумывать новые статус-коды — или использовать существующие не по назначению.