mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-05-25 22:08:06 +02:00
Proofreading
This commit is contained in:
parent
b072162375
commit
68ad84fdab
@ -70,7 +70,7 @@ Older platform versions contribute to fragmentation just like older app versions
|
||||
|
||||
The most challenging aspect here is that not only does incremental progress, in the form of new platforms and protocols, necessitate changes to the API, but also the vulgar influence of trends. Several years ago realistic 3D icons were popular, but since then, public taste has changed in favor of flat and abstract ones. UI component developers had to follow the fashion, rebuilding their libraries by either shipping new icons or replacing the old ones. Similarly, the current trend of integrating the “night mode” feature has become widespread, demanding changes in a wide range of APIs.
|
||||
|
||||
#### Backwards-Compatible Specifications
|
||||
#### Backward-Compatible Specifications
|
||||
|
||||
In the case of the API-first approach, the backward compatibility problem adds another dimension: the specification and code generation based on it. It becomes possible to break backward compatibility without breaking the spec (for example, by introducing eventual consistency instead of strict consistency) — and vice versa, modify the spec in a backward-incompatible manner without changing anything in the protocol and therefore not affecting existing integrations at all (for example, by replacing `additionalProperties: false` with `true` in OpenAPI).
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
### [On the Waterline of the Iceberg][back-compat-iceberg-waterline]
|
||||
|
||||
Before we start talking about the extensible API design, we should discuss the hygienic minimum. A huge number of problems would have never happened if API vendors had paid more attention to marking their area of responsibility.
|
||||
Before we start talking about extensible API design, we should discuss the hygienic minimum. A huge number of problems would have never happened if API vendors had paid more attention to marking their area of responsibility.
|
||||
|
||||
#### Provide a Minimal Amount of Functionality
|
||||
##### Provide a Minimal Amount of Functionality
|
||||
|
||||
At any moment in its lifetime, your API is like an iceberg: it comprises an observable (i.e., documented) part and a hidden one, undocumented. If the API is designed properly, these two parts correspond to each other just like the above-water and under-water parts of a real iceberg do, i.e. one to ten. Why so? Because of two obvious reasons.
|
||||
|
||||
@ -10,22 +10,22 @@ At any moment in its lifetime, your API is like an iceberg: it comprises an obse
|
||||
|
||||
* 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 until you really need to. It might be reformulated like that: every entity, every field, and every public API method is a *product decision*. There must be solid *product* reasons why some functionality is exposed.
|
||||
A rule of thumb is very simple: if some functionality might be withheld — then never expose it until you really need to. It might be reformulated like this: every entity, every field, and every public API method is a *product decision*. There must be solid *product* reasons why some functionality is exposed.
|
||||
|
||||
##### 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. The policy on such “findings” must be articulated explicitly. At the very least, in the case of such non-authorized usage of undocumented functionality, you might refer to the docs and be within your rights in the eyes of the community.
|
||||
|
||||
However, API developers often legitimize such gray zones themselves, for example, by:
|
||||
|
||||
* returning undocumented fields in endpoints responses;
|
||||
* returning undocumented fields in endpoint responses;
|
||||
* using private functionality in code samples — in the docs, while responding to support messages, in conference talks, etc.
|
||||
|
||||
One cannot make a partial commitment. Either you guarantee this code will always work or do not slip the slightest note such functionality exists.
|
||||
|
||||
##### Codify Implicit Agreements
|
||||
|
||||
The third principle is much less obvious. Pay close attention to the code which you're suggesting developers to develop: are there any conventions that you consider evident, but never wrote them down?
|
||||
The third principle is much less obvious. Pay close attention to the code that you're suggesting developers write: are there any conventions that you consider self-evident but never wrote down?
|
||||
|
||||
**Example \#1**. Let's take a look at this order processing SDK example:
|
||||
```
|
||||
@ -35,9 +35,7 @@ let order = api.createOrder();
|
||||
let status = api.getStatus(order.id);
|
||||
```
|
||||
|
||||
Let's imagine that you're struggling with scaling your service, and at some point moved to the asynchronous replication of the database. This would lead to the situation when querying for the order status right after the order creation might return `404` if an asynchronous replica hasn't got the update yet. In fact, thus we abandon a strict [consistency policy](https://en.wikipedia.org/wiki/Consistency_model) in a favor of an eventual one.
|
||||
|
||||
What would be the result? The code above will stop working. A user 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, they would not expect this to happen at all.
|
||||
Let's imagine that you're struggling with scaling your service, and at some point switched to eventual consistency, as we discussed in the [corresponding chapter](#api-patterns-weak-consistency). What would be the result? The code above will stop working. A user creates an order, then tries to get its status but receives an error instead. It's very hard to predict what approach developers would implement to tackle this error. They probably would not expect this to happen at all.
|
||||
|
||||
You may say something like, “But we've never promised 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 this:
|
||||
|
||||
@ -59,9 +57,9 @@ if (status) {
|
||||
}
|
||||
```
|
||||
|
||||
We presume we may skip the explanations why such code must never be written under any circumstances. If you're really providing a non-strictly consistent API, then either the `createOrder` operation must be asynchronous and return the result when all replicas are synchronized, or the retry policy must be hidden inside the `getStatus` operation implementation.
|
||||
We presume we may skip the explanations of why such code must never be written under any circumstances. If you're really providing a non-strictly consistent API, then either the `createOrder` operation must be asynchronous and return the result when all replicas are synchronized, or the retry policy must be hidden inside the `getStatus` operation implementation.
|
||||
|
||||
If you failed to describe the eventual consistency in the first place, then you simply couldn't make these changes in the API. You will effectively break backward compatibility, which will lead to huge problems with your customers' apps, intensified by the fact they can't be simply reproduced by QA engineers.
|
||||
If you failed to describe the eventual consistency in the first place, then you simply couldn't make these changes in the API. You will effectively break backward compatibility, which will lead to huge problems with your customers' apps, intensified by the fact that they can't be simply reproduced by QA engineers.
|
||||
|
||||
**Example \#2**. Take a look at the following code:
|
||||
|
||||
@ -75,11 +73,11 @@ let promise = new Promise(
|
||||
resolve();
|
||||
```
|
||||
|
||||
This code presumes that the callback function passed to a `new Promise` will be executed synchronously, and the `resolve` variable will be initialized before the `resolve()` function call is executed. But this assumption is based on nothing: there are no clues indicating the `new Promise` constructor executes the callback function synchronously.
|
||||
This code presumes that the callback function passed to a `new Promise` will be executed synchronously, and the `resolve` variable will be initialized before the `resolve()` function call is executed. But this assumption is based on nothing: there are no clues indicating that the `new Promise` constructor executes the callback function synchronously.
|
||||
|
||||
Of course, the developers of the language standard can afford such tricks; but you as an API developer cannot. You must at least document this behavior and make the signatures point to it; actually, the best practice is to avoid such conventions, since they are simply unobvious while reading the code. And of course, under no circumstances dare you change this behavior to an asynchronous one.
|
||||
Of course, the developers of the language standard can afford such tricks; but you as an API developer cannot. You must at least document this behavior and make the signatures point to it. Actually, the best practice is to avoid such conventions since they are simply not obvious when reading the code. And of course, under no circumstances dare you change this behavior to an asynchronous one.
|
||||
|
||||
**Example \#3**. Imagine you're providing animations API, which includes two independent functions:
|
||||
**Example \#3**. Imagine you're providing an animations API, which includes two independent functions:
|
||||
|
||||
```
|
||||
// Animates object's width,
|
||||
@ -95,9 +93,9 @@ object.observe(
|
||||
);
|
||||
```
|
||||
|
||||
A question arises: how frequently and at what time fractions the `observerFunction` will be called? Let's assume in the first SDK version we emulated step-by-step animation at 10 frames per second. Then the `observerFunction` will be called 10 times, getting values `'140px'`, `'180px'`, etc., up to `'500px'`. But then, in a new API version, we have switched to implementing both functions atop of a system's native functionality — and so you simply don't know, when and how frequently the `observerFunction` will be called.
|
||||
A question arises: how frequently and at what time fractions will the `observerFunction` be called? Let's assume in the first SDK version we emulated step-by-step animation at 10 frames per second. Then the `observerFunction` will be called 10 times, getting values `'140px'`, `'180px'`, etc., up to `'500px'`. But then, in a new API version, we have switched to implementing both functions atop the operating system's native functionality. Therefore, you simply don't know when and how frequently the `observerFunction` will be called.
|
||||
|
||||
Just changing call frequency might result in making some code dysfunctional — for example, if the callback function makes some complex calculations, and no throttling is implemented since developers just relied on your SDK's built-in throttling. And if the `observerFunction` ceases to be called when exactly '500px' is reached because of some system algorithms specifics, some code will be broken beyond any doubt.
|
||||
Just changing the call frequency might result in making some code dysfunctional. For example, if the callback function performs some complex calculations and no throttling is implemented because developers relied on your SDK's built-in throttling. Additionally, if the `observerFunction` ceases to be called exactly when the `'500px'` value is reached due to system algorithm specifics, some code will be broken beyond any doubt.
|
||||
|
||||
In this example, you should document the concrete contract (how often the observer function is called) and stick to it even if the underlying technology is changed.
|
||||
|
||||
@ -127,18 +125,18 @@ GET /v1/orders/{id}/events/history
|
||||
]}
|
||||
```
|
||||
|
||||
Suppose at some moment we decided to allow trustworthy clients to get their coffee in advance before the payment is confirmed. So an order will jump straight to `"preparing_started"` or even `"ready"` without a `"payment_approved"` event being emitted. It might appear to you that this modification *is* backwards-compatible since you've never really promised any specific event order being maintained, but it is not.
|
||||
Suppose at some moment we decided to allow trustworthy clients to get their coffee in advance before the payment is confirmed. So an order will jump straight to `"preparing_started"` or even `"ready"` without a `"payment_approved"` event being emitted. It might appear to you that this modification *is* backward-compatible since you've never really promised any specific event order being maintained, but it is not.
|
||||
|
||||
Let's assume that a developer (probably, your company's business partner) wrote some code implementing some valuable business procedures, for example, gathering income and expenses analytics. It's quite logical to expect this code operates a state machine, which switches from one state to another depending on getting (or getting not) specific events. This analytical code will be broken if the event order changes. In the best-case scenario, a developer will get some exceptions and will have to cope with the error's cause; in the worst case, partners will operate wrong statistics for an indefinite period of time until they find the issue.
|
||||
Let's assume that a developer (probably your company's business partner) wrote some code implementing some valuable business procedures, for example, gathering income and expenses analytics. It's quite logical to expect this code operates a state machine that switches from one state to another depending on specific events. This analytical code will be broken if the event order changes. In the best-case scenario, a developer will get some exceptions and will have to cope with the error's cause. In the worst case, partners will operate the incorrect statistics for an indefinite period of time until they find the issue.
|
||||
|
||||
A proper decision would be, first, documenting the event order and the allowed states; second, continuing generating the "payment_approved" event before the "preparing_started" one (since you're making a decision to prepare that order, so you're in fact approving the payment) and add extended payment information.
|
||||
A proper decision would be, first, documenting the event order and the allowed states; second, continuing to generate the "payment_approved" event before the "preparing_started" one (since you're making a decision to prepare that order, so you're in fact approving the payment) and add extended payment information.
|
||||
|
||||
This example leads us to the last rule.
|
||||
|
||||
##### Product Logic Must Be Backwards-Compatible as Well
|
||||
##### Product Logic Must Be Backward-Compatible as Well
|
||||
|
||||
State transition graph, event order, possible causes of status changes — such critical things must be documented. However, not every piece of business logic might be defined in a form of a programmatical contract; some cannot be represented in a machine-readable form at all.
|
||||
State transition graph, event order, possible causes of status changes, etc. — such critical things must be documented. However, not every piece of business logic can be defined in the form of a programmable contract; some cannot be represented in a machine-readable form at all.
|
||||
|
||||
Imagine that one day you start 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 taking phone calls. A client may contact the call center to cancel an order. You might even make this functionality *technically* backward-compatible by introducing new fields to the “order” entity. But the end-user might simply *know* the number and call it even if the app wasn't suggesting anything like that. The partner's business analytical code might be broken as well or start displaying weather on Mars since it was written without knowing about the possibility of canceling orders in circumvention of the partner's systems.
|
||||
|
||||
A *technically* correct decision would be to add a “canceling via call center allowed” parameter to the order creation function. Conversely, call center operators might only cancel those orders which were created with this flag set. But that would be a bad decision from a *product* point of view as it's quite unobvious to users that they can cancel some orders by phone and can't cancel others. The only “good” decision in this situation is to foresee the possibility of external order cancellations in the first place. If you haven't foreseen it, your only option is the “Serenity Notepad” 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 might only cancel those orders that were created with this flag set. But that would be a bad decision from a *product* point of view because it is not obvious to users that they can cancel some orders by phone and not others. The only “good” decision in this situation is to foresee the possibility of external order cancellations in the first place. If you haven't foreseen it, your only option is the “Serenity Notepad” that will be discussed in the last chapter of this Section.
|
@ -44,6 +44,6 @@ There are obvious local problems with this approach (like the inconsistency in f
|
||||
|
||||
##### Keep a Notepad
|
||||
|
||||
Whatever tips and tricks described in the previous chapters you use, it's often quite probable that you can't do *anything* to prevent API inconsistencies from piling up. It's possible to reduce the speed of this stockpiling, foresee some problems, and have some interface durability reserved for future use. But one can't foresee *everything*. At this stage, many developers tend to make some rash decisions, e.g., releasing a backwards-incompatible minor version to fix some design flaws.
|
||||
Whatever tips and tricks described in the previous chapters you use, it's often quite probable that you can't do *anything* to prevent API inconsistencies from piling up. It's possible to reduce the speed of this stockpiling, foresee some problems, and have some interface durability reserved for future use. But one can't foresee *everything*. At this stage, many developers tend to make some rash decisions, e.g., releasing a backward-incompatible minor version to fix some design flaws.
|
||||
|
||||
We highly recommend never doing that. Remember that the API is also a multiplier of your mistakes. What we recommend is to keep a serenity notepad — to write down the lessons learned, and not to forget to apply this knowledge when a new major API version is released.
|
||||
|
@ -23,7 +23,7 @@ As both static and behavioral analyses are heuristic, it's highly desirable to n
|
||||
|
||||
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 APIs it might be problematic, especially if you initially neglected the “Stipulate Restrictions” rule we've given in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter. In many cases, you will have to impose this responsibility on partners (i.e., 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 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.
|
||||
**NB**. Instead of captcha, there might be 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 backward-compatible manner.
|
||||
|
||||
Other popular mechanics of identifying robots include offering a bait (“honeypot”) or employing the execution environment checks (starting from rather trivial ones like executing JavaScript on the webpage and ending with sophisticated techniques of checking application integrity checksums).
|
||||
|
||||
|
@ -14,11 +14,11 @@ In this aspect, integrating with large companies that have a dedicated software
|
||||
|
||||
Another aspect crucial to interacting with large integrators is supporting a zoo of platforms (browsers, programming languages, protocols, operating systems) and their versions. As usual, big companies have their own policies on which platforms they support, and these policies might sometimes contradict common sense. (Let's say, it's rather a time to abandon TLS 1.2, but many integrators continue working through this protocol, or even the earlier ones.)
|
||||
|
||||
Formally speaking, ceasing support of a platform *is* a backwards-incompatible change, and might lead to breaking some integration for some end users. So it's highly important to have clearly formulated policies regarding which platforms are supported based on which criteria. In the case of mass public APIs, that's usually simple (like, API vendor promises to support platforms that have more than N% penetration, or, even easier, just last M versions of a platform); in the case of commercial APIs, it's always a bargain based on the estimations, how much will non-supporting a specific platform would cost to a company. And of course, the outcome of the bargain must be stated in the contracts — what exactly you're promising to support during which period of time.
|
||||
Formally speaking, ceasing support of a platform *is* a backward-incompatible change, and might lead to breaking some integration for some end users. So it's highly important to have clearly formulated policies regarding which platforms are supported based on which criteria. In the case of mass public APIs, that's usually simple (like, API vendor promises to support platforms that have more than N% penetration, or, even easier, just last M versions of a platform); in the case of commercial APIs, it's always a bargain based on the estimations, how much will non-supporting a specific platform would cost to a company. And of course, the outcome of the bargain must be stated in the contracts — what exactly you're promising to support during which period of time.
|
||||
|
||||
#### Moving Forward
|
||||
|
||||
Finally, apart from those specific issues, your customers must be caring about more general questions: could they trust you? Could they rely on your API evolving, absorbing modern trends, or will they eventually find the integration with your API on the scrapyard of history? Let's be honest: given all the uncertainties of the API product vision, we are very much interested in the answers as well. Even the Roman viaduct, though remaining backwards-compatible for two thousand years, has been a very archaic and non-reliable way of solving customers' problems for quite a long time.
|
||||
Finally, apart from those specific issues, your customers must be caring about more general questions: could they trust you? Could they rely on your API evolving, absorbing modern trends, or will they eventually find the integration with your API on the scrapyard of history? Let's be honest: given all the uncertainties of the API product vision, we are very much interested in the answers as well. Even the Roman viaduct, though remaining backward-compatible for two thousand years, has been a very archaic and non-reliable way of solving customers' problems for quite a long time.
|
||||
|
||||
You might work with these customer expectations by publishing roadmaps. It's quite common that many companies avoid publicly announcing their concrete plans (for a reason, of course). Nevertheless, in the case of APIs, we strongly recommend providing the roadmaps, even if they are tentative and lack precise dates — *especially* if we talk about deprecating some functionality. Announcing these promises (given the company keeps them, of course) is a very important competitive advantage to every kind of consumer.
|
||||
|
||||
|
@ -35,9 +35,7 @@ let order = api.createOrder();
|
||||
let status = api.getStatus(order.id);
|
||||
```
|
||||
|
||||
Предположим, что в какой-то момент при масштабировании вашего сервиса вы пришли к асинхронной репликации базы данных и разрешили чтение из реплики. Это приведёт к тому, что после создания заказа следующее обращение к его статусу по id может вернуть `404`, если оно пришлось на асинхронную реплику, до которой ещё не дошли последние изменения из мастера. Фактически, вы сменили [политику консистентности](https://en.wikipedia.org/wiki/Consistency_model) со strong на eventual.
|
||||
|
||||
К чему это приведёт? К тому, что код выше перестанет работать. Разработчик создал заказ, пытается получить его статус — и получает ошибку. Очень тяжело предсказать, какую реакцию на эту ошибку предусмотрят разработчики — вероятнее всего, никакую.
|
||||
Предположим, что в какой-то момент при масштабировании вашего сервиса вы пришли к событийной консистентности (см. [соответствующую главу](#api-patterns-weak-consistency)). К чему это приведёт? К тому, что код выше перестанет работать. Разработчик создал заказ, пытается получить его статус — и получает ошибку. Очень тяжело предсказать, какую реакцию на эту ошибку предусмотрят разработчики — вероятнее всего, никакую.
|
||||
|
||||
Вы можете сказать: «Позвольте, но мы нигде и не обещали строгую консистентность!» — и это будет, конечно, неправдой. Вы можете так сказать если, и только если, вы действительно в документации метода `createOrder` явно описали нестрогую консистентность, а все ваши примеры использования SDK написаны как-то так:
|
||||
|
||||
|
@ -91,7 +91,7 @@ HTTP-глагол определяет два важных свойства HTTP
|
||||
| POST | Обрабатывает запрос в соответствии со своим внутренним устройством | нет | нет | да |
|
||||
| PATCH | Модифицирует (частично перезаписывает) ресурс согласно данным, переданным в теле запроса | нет | нет | да |
|
||||
|
||||
Важное свойство модифицирующих идемпотентных глаголов — это то, что **URL запроса является его ключом идемпотентности**. `PUT /url` полностью перезаписывает ресурс, заданный своим URL (`/url`), и, таким образом, повтор запроса не изменяет ресурс. Аналогично, повторный вызов `DELETE /url` должен оставить систему в том же состоянии (ресурс `/url` удалён). Учитывая, что метод `GET /url` семантически должен вернуть представление целевого ресурса `/url`, то, если этот метод реализован, он должен возвращать консистентное предыдущим `PUT` / `DELETE` представление. Если ресурс был перезаписан через `PUR /url`, `GET /url` должен вернуть представление, соответствующее переданном в `PUT /url` телу (в случае JSON-over-HTTP API это, как правило, просто означает, что `GET /url` возвращает в точности тот же контент, чтобы передан в `PUT /url`, с точностью до значений полей по умолчанию). `DELETE /url` обязан удалить указанный ресурс — так, что `GET /url` должен вернуть `404` или `410`.
|
||||
Важное свойство модифицирующих идемпотентных глаголов — это то, что **URL запроса является его ключом идемпотентности**. `PUT /url` полностью перезаписывает ресурс, заданный своим URL (`/url`), и, таким образом, повтор запроса не изменяет ресурс. Аналогично, повторный вызов `DELETE /url` должен оставить систему в том же состоянии (ресурс `/url` удалён). Учитывая, что метод `GET /url` семантически должен вернуть представление целевого ресурса `/url`, то, если этот метод реализован, он должен возвращать консистентное предыдущим `PUT` / `DELETE` представление. Если ресурс был перезаписан через `PUT /url`, `GET /url` должен вернуть представление, соответствующее переданном в `PUT /url` телу (в случае JSON-over-HTTP API это, как правило, просто означает, что `GET /url` возвращает в точности тот же контент, чтобы передан в `PUT /url`, с точностью до значений полей по умолчанию). `DELETE /url` обязан удалить указанный ресурс — так, что `GET /url` должен вернуть `404` или `410`.
|
||||
|
||||
Идемпотентность и симметричность методов `GET` / `PUT` / `DELETE` влечёт за собой запрет `GET` и `DELETE` иметь тело запроса (поскольку этому телу невозможно приписать никакой осмысленной роли). Однако (по-видимому в связи с тем, что многие разработчики попросту не знают семантику этих методов) распространённое ПО веб-серверов обычно разрешает этим методам иметь тело запроса и транслирует его дальше к коду обработки эндпойнта (использование этой практики мы решительно не рекомендуем).
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Backward compatibility is a *temporal* characteristic of your API. An obligation to maintain backward compatibility is the crucial point where API development differs from software development in general.
|
||||
|
||||
Of course, backward 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, backward compatibility isn't an absolute. In some subject areas shipping new backward-incompatible API versions is a routine. Nevertheless, every time you deploy a new backward-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 charge such a tax. Furthermore, they may introduce penalties for those who refuse to adapt their code to new API versions, up to disabling their applications.
|
||||
|
||||
|
@ -3,12 +3,12 @@
|
||||
Here and throughout this book, we firmly stick to [semver](https://semver.org/) principles of versioning.
|
||||
|
||||
1. API versions are denoted with three numbers, i.e., `1.2.3`.
|
||||
2. The first number (a major version) increases when backwards-incompatible changes in the API are introduced.
|
||||
2. The first number (a major version) increases when backward-incompatible changes in the API are introduced.
|
||||
3. The second number (a minor version) increases when new functionality is added to the API, keeping backward compatibility intact.
|
||||
4. The third number (a patch) increases when a new API version contains bug fixes only.
|
||||
|
||||
Sentences “a major API version” and “new API version, containing backwards-incompatible changes” are therefore to be considered equivalent ones.
|
||||
Sentences “a major API version” and “new API version, containing backward-incompatible changes” are therefore to be considered equivalent ones.
|
||||
|
||||
It is usually (though not necessary) agreed that the last stable API release 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 release that is backwards-compatible to the `1.2.3` version”) or additional shortcuts (for example, `1.2-beta` to refer to the last beta release 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 release 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 release that is backward-compatible to the `1.2.3` version”) or additional shortcuts (for example, `1.2-beta` to refer to the last beta release 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 Backward Compatibility Problem Statement](#back-compat-statement)” chapter.
|
||||
|
@ -26,7 +26,7 @@ We could say that *we break backward compatibility to introduce new features to
|
||||
|
||||
These arguments could be summarized frankly as “the API vendors 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 backward-compatible API release.
|
||||
|
||||
Let us remind the reader that [an API is a bridge](#intro-api-definition), 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 rewrite *their* code.
|
||||
|
||||
@ -36,7 +36,7 @@ Apart from our aspirations to change the API architecture, three other tectonic
|
||||
|
||||
When you shipped the very first API version, and the very first clients started to use it, the situation was perfect. There was only one version, and all clients were using only it. When this perfection ends, two scenarios are possible.
|
||||
|
||||
1. If the platform allows for fetching code on-demand as the good old Web does, and you weren't too lazy to implement that code-on-demand feature (in a form of a platform SDK — for example, JS API), then the evolution of your API is more or less under your control. Maintaining backward compatibility effectively means keeping *the client library* backwards-compatible. As for client-server interaction, you're free.
|
||||
1. If the platform allows for fetching code on-demand as the good old Web does, and you weren't too lazy to implement that code-on-demand feature (in a form of a platform SDK — for example, JS API), then the evolution of your API is more or less under your control. Maintaining backward compatibility effectively means keeping *the client library* backward-compatible. As for client-server interaction, you're free.
|
||||
|
||||
It doesn't mean that you can't break backward compatibility. You still can make a mess with cache-control headers or just overlook a bug in the code. Besides, even code-on-demand systems don't get updated instantly. The author of this book faced a situation when users were deliberately keeping a browser tab open *for weeks* to get rid of updates. But still, you usually don't have to support more than two API versions — the last one and the penultimate one. Furthermore, you may try to rewrite the previous major version of the library, implementing it on top of the actual API version.
|
||||
|
||||
@ -66,7 +66,7 @@ Let us also stress that vendors of low-level API are not always as resolute rega
|
||||
|
||||
#### Platform Drift
|
||||
|
||||
Finally, there is a third side to the 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 and new arrangements made, some of them being backwards-incompatible, and nothing could be done about that.
|
||||
Finally, there is a third side to the 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 and new arrangements made, some of them being backward-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 get updated at all, since newer platform versions require newer devices.
|
||||
|
||||
@ -106,7 +106,7 @@ Indeed, with the growth of the number of users, the “rollback the API version
|
||||
|
||||
The important (and undeniable) advantage of the *semver* system is that it provides the proper version granularity:
|
||||
|
||||
* stating the first digit (major version) allows for getting a backwards-compatible version of the API;
|
||||
* stating the first digit (major version) allows for getting a backward-compatible version of the API;
|
||||
* stating two digits (major and minor versions) allows guaranteeing that some functionality that was added after the initial release will be available;
|
||||
* finally, stating all three numbers (major version, minor version, and patch) allows for fixing a concrete API release with all its specificities (and errors), which — theoretically — means that the integration will remain operable till this version is physically available.
|
||||
|
||||
|
@ -127,7 +127,7 @@ GET /v1/orders/{id}/events/history
|
||||
]}
|
||||
```
|
||||
|
||||
Suppose at some moment we decided to allow trustworthy clients to get their coffee in advance before the payment is confirmed. So an order will jump straight to `"preparing_started"` or even `"ready"` without a `"payment_approved"` event being emitted. It might appear to you that this modification *is* backwards-compatible since you've never really promised any specific event order being maintained, but it is not.
|
||||
Suppose at some moment we decided to allow trustworthy clients to get their coffee in advance before the payment is confirmed. So an order will jump straight to `"preparing_started"` or even `"ready"` without a `"payment_approved"` event being emitted. It might appear to you that this modification *is* backward-compatible since you've never really promised any specific event order being maintained, but it is not.
|
||||
|
||||
Let's assume that a developer (probably, your company's business partner) wrote some code implementing some valuable business procedures, for example, gathering income and expenses analytics. It's quite logical to expect this code operates a state machine, which switches from one state to another depending on getting (or getting not) specific events. This analytical code will be broken if the event order changes. In the best-case scenario, a developer will get some exceptions and will have to cope with the error's cause; in the worst case, partners will operate wrong statistics for an indefinite period of time until they find the issue.
|
||||
|
||||
@ -135,10 +135,10 @@ A proper decision would be, first, documenting the event order and the allowed s
|
||||
|
||||
This example leads us to the last rule.
|
||||
|
||||
##### Product Logic Must Be Backwards-Compatible as Well
|
||||
##### Product Logic Must Be Backward-Compatible as Well
|
||||
|
||||
State transition graph, event order, possible causes of status changes — such critical things must be documented. However, not every piece of business logic might be defined in a form of a programmatical contract; some cannot be represented in a machine-readable form 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* backward-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 might only cancel those orders which were created with this flag set. But that would be a bad decision from a *product* point of view as it's quite unobvious to users that they can cancel some orders by phone and can't cancel others. The only “good” decision in this situation is to foresee the possibility of external order cancellations in the first place. If you haven't foreseen it, your only option is the “Serenity Notepad” to be discussed in the last chapter of this Section.
|
@ -44,6 +44,6 @@ There are obvious local problems with this approach (like the inconsistency in f
|
||||
|
||||
##### Keep a Notepad
|
||||
|
||||
Whatever tips and tricks described in the previous chapters you use, it's often quite probable that you can't do *anything* to prevent API inconsistencies from piling up. It's possible to reduce the speed of this stockpiling, foresee some problems, and have some interface durability reserved for future use. But one can't foresee *everything*. At this stage, many developers tend to make some rash decisions, e.g., releasing a backwards-incompatible minor version to fix some design flaws.
|
||||
Whatever tips and tricks described in the previous chapters you use, it's often quite probable that you can't do *anything* to prevent API inconsistencies from piling up. It's possible to reduce the speed of this stockpiling, foresee some problems, and have some interface durability reserved for future use. But one can't foresee *everything*. At this stage, many developers tend to make some rash decisions, e.g., releasing a backward-incompatible minor version to fix some design flaws.
|
||||
|
||||
We highly recommend never doing that. Remember that the API is also a multiplier of your mistakes. What we recommend is to keep a serenity notepad — to write down the lessons learned, and not to forget to apply this knowledge when a new major API version is released.
|
||||
|
@ -23,7 +23,7 @@ As both static and behavioral analyses are heuristic, it's highly desirable to n
|
||||
|
||||
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 APIs it might be problematic, especially if you initially neglected the “Stipulate Restrictions” rule we've given in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter. In many cases, you will have to impose this responsibility on partners (i.e., 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 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.
|
||||
**NB**. Instead of captcha, there might be 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 backward-compatible manner.
|
||||
|
||||
Other popular mechanics of identifying robots include offering a bait (“honeypot”) or employing the execution environment checks (starting from rather trivial ones like executing JavaScript on the webpage and ending with sophisticated techniques of checking application integrity checksums).
|
||||
|
||||
|
@ -14,11 +14,11 @@ In this aspect, integrating with large companies that have a dedicated software
|
||||
|
||||
Another aspect crucial to interacting with large integrators is supporting a zoo of platforms (browsers, programming languages, protocols, operating systems) and their versions. As usual, big companies have their own policies on which platforms they support, and these policies might sometimes contradict common sense. (Let's say, it's rather a time to abandon TLS 1.2, but many integrators continue working through this protocol, or even the earlier ones.)
|
||||
|
||||
Formally speaking, ceasing support of a platform *is* a backwards-incompatible change, and might lead to breaking some integration for some end users. So it's highly important to have clearly formulated policies regarding which platforms are supported based on which criteria. In the case of mass public APIs, that's usually simple (like, API vendor promises to support platforms that have more than N% penetration, or, even easier, just last M versions of a platform); in the case of commercial APIs, it's always a bargain based on the estimations, how much will non-supporting a specific platform would cost to a company. And of course, the outcome of the bargain must be stated in the contracts — what exactly you're promising to support during which period of time.
|
||||
Formally speaking, ceasing support of a platform *is* a backward-incompatible change, and might lead to breaking some integration for some end users. So it's highly important to have clearly formulated policies regarding which platforms are supported based on which criteria. In the case of mass public APIs, that's usually simple (like, API vendor promises to support platforms that have more than N% penetration, or, even easier, just last M versions of a platform); in the case of commercial APIs, it's always a bargain based on the estimations, how much will non-supporting a specific platform would cost to a company. And of course, the outcome of the bargain must be stated in the contracts — what exactly you're promising to support during which period of time.
|
||||
|
||||
#### Moving Forward
|
||||
|
||||
Finally, apart from those specific issues, your customers must be caring about more general questions: could they trust you? Could they rely on your API evolving, absorbing modern trends, or will they eventually find the integration with your API on the scrapyard of history? Let's be honest: given all the uncertainties of the API product vision, we are very much interested in the answers as well. Even the Roman viaduct, though remaining backwards-compatible for two thousand years, has been a very archaic and non-reliable way of solving customers' problems for quite a long time.
|
||||
Finally, apart from those specific issues, your customers must be caring about more general questions: could they trust you? Could they rely on your API evolving, absorbing modern trends, or will they eventually find the integration with your API on the scrapyard of history? Let's be honest: given all the uncertainties of the API product vision, we are very much interested in the answers as well. Even the Roman viaduct, though remaining backward-compatible for two thousand years, has been a very archaic and non-reliable way of solving customers' problems for quite a long time.
|
||||
|
||||
You might work with these customer expectations by publishing roadmaps. It's quite common that many companies avoid publicly announcing their concrete plans (for a reason, of course). Nevertheless, in the case of APIs, we strongly recommend providing the roadmaps, even if they are tentative and lack precise dates — *especially* if we talk about deprecating some functionality. Announcing these promises (given the company keeps them, of course) is a very important competitive advantage to every kind of consumer.
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
"author": "Sergey Konstantinov",
|
||||
"chapter": "Chapter",
|
||||
"toc": "Table of Contents",
|
||||
"description": "Designing APIs is a very special skill: API is a multiplier to both your opportunities and mistakes. This book is written to share the expertise and describe the best practices in designing and developing APIs. In Section I, we'll discuss the API architecture as a concept: how to build the hierarchy properly, from high-level planning down to final interfaces. Section II is dedicated to expanding existing APIs in a backwards-compatible manner. Finally, in Section III we will talk about the API as a product.",
|
||||
"description": "Designing APIs is a very special skill: API is a multiplier to both your opportunities and mistakes. This book is written to share the expertise and describe the best practices in designing and developing APIs. In Section I, we'll discuss the API architecture as a concept: how to build the hierarchy properly, from high-level planning down to final interfaces. Section II is dedicated to expanding existing APIs in a backward-compatible manner. Finally, in Section III we will talk about the API as a product.",
|
||||
"locale": "en_US",
|
||||
"file": "API",
|
||||
"aboutMe": {
|
||||
@ -69,7 +69,7 @@
|
||||
"pageTitle": "Front Page",
|
||||
"contents": [
|
||||
"<p>The API-first development is one of the hottest technical topics nowadays, since many companies started to realize that API serves as a multiplicator to their opportunities—but it also amplifies the design mistakes as well.</p>",
|
||||
"<p>This book is written to share the expertise and describe the best practices in designing and developing APIs. In Section I, we'll discuss the API architecture as a concept: how to build the hierarchy properly, from high-level planning down to final interfaces. Section II is dedicated to expanding existing APIs in a backwards-compatible manner. Finally, in Section III we will talk about the API as a product.</p>",
|
||||
"<p>This book is written to share the expertise and describe the best practices in designing and developing APIs. In Section I, we'll discuss the API architecture as a concept: how to build the hierarchy properly, from high-level planning down to final interfaces. Section II is dedicated to expanding existing APIs in a backward-compatible manner. Finally, in Section III we will talk about the API as a product.</p>",
|
||||
"<p class=\"text-align-left\">Illustrations & inspiration by Maria Konstantinova · <a href=\"https://www.instagram.com/art.mari.ka/\">art.mari.ka</a></p>",
|
||||
"<img class=\"cc-by-nc-img\" alt=\"Creative Commons «Attribution-NonCommercial» Logo\" src=\"https://i.creativecommons.org/l/by-nc/4.0/88x31.png\"/>",
|
||||
"<p class=\"cc-by-nc\">This book is distributed under the <a href=\"http://creativecommons.org/licenses/by-nc/4.0/\">Creative Commons Attribution-NonCommercial 4.0 International licence</a>.</p>"
|
||||
@ -85,7 +85,7 @@
|
||||
"support": ["patreon", "kindle"],
|
||||
"content": [
|
||||
"<p>The API-first development is one of the hottest technical topics nowadays, since many companies started to realize that API serves as a multiplicator to their opportunities—but it also amplifies the design mistakes as well.</p>",
|
||||
"<p>This book is written to share the expertise and describe the best practices in designing and developing APIs. In Section I, we'll discuss the API architecture as a concept: how to build the hierarchy properly, from high-level planning down to final interfaces. Section II is dedicated to expanding existing APIs in a backwards-compatible manner. Finally, in Section III we will talk about the API as a product.</p>",
|
||||
"<p>This book is written to share the expertise and describe the best practices in designing and developing APIs. In Section I, we'll discuss the API architecture as a concept: how to build the hierarchy properly, from high-level planning down to final interfaces. Section II is dedicated to expanding existing APIs in a backward-compatible manner. Finally, in Section III we will talk about the API as a product.</p>",
|
||||
"<p>Illustration & inspiration: <a href=\"https://www.instagram.com/art.mari.ka/\">art.mari.ka</a>.</p>"
|
||||
],
|
||||
"download": "You might download ‘The API’ in",
|
||||
|
Loading…
x
Reference in New Issue
Block a user