You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-07-12 22:50:21 +02:00
fresh build
This commit is contained in:
BIN
docs/API.en.docx
BIN
docs/API.en.docx
Binary file not shown.
BIN
docs/API.en.epub
BIN
docs/API.en.epub
Binary file not shown.
@ -1480,24 +1480,27 @@ The invalid price error is resolvable: a client could obtain a new price offer a
|
||||
<p>Entity name must explicitly tell what it does and what side effects to expect while using it.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>// Cancels an order
|
||||
GET /orders/cancellation
|
||||
order.canceled = true;
|
||||
</code></pre>
|
||||
<p>It's quite a surprise that accessing the <code>cancellation</code> resource (what is it?) with the non-modifying <code>GET</code> method actually cancels an order.</p>
|
||||
<p>It's unobvious that a state field might be set, and that this operation will cancel the order.</p>
|
||||
<p><strong>Better</strong>:</p>
|
||||
<pre><code>// Cancels an order
|
||||
POST /orders/cancel
|
||||
order.cancel();
|
||||
</code></pre>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>// Returns aggregated statistics
|
||||
// since the beginning of time
|
||||
GET /orders/statistics
|
||||
orders.getStats()
|
||||
</code></pre>
|
||||
<p>Even if the operation is non-modifying but computationally expensive, you should explicitly indicate that, especially if clients got charged for computational resource usage. Even more so, default values must not be set in a manner leading to maximum resource consumption.</p>
|
||||
<p><strong>Better</strong>:</p>
|
||||
<pre><code>// Returns aggregated statistics
|
||||
<pre><code>// Calculates and returns
|
||||
// aggregated statistics
|
||||
// for a specified period of time
|
||||
POST /v1/orders/statistics/aggregate
|
||||
{ "begin_date", "end_date" }
|
||||
orders.calculateAggregatedStats({
|
||||
begin_date,
|
||||
end_date
|
||||
});
|
||||
</code></pre>
|
||||
<p><strong>Try to design function signatures to be absolutely transparent about what the function does, what arguments it takes, and what's the result</strong>. While reading a code working with your API, it must be easy to understand what it does without reading docs.</p>
|
||||
<p>Two important implications:</p>
|
||||
@ -1514,6 +1517,8 @@ POST /v1/orders/statistics/aggregate
|
||||
or<br>
|
||||
<code>"duration": "5000ms"</code><br>
|
||||
or<br>
|
||||
<code>"iso_duration": "PT5S"</code><br>
|
||||
or<br>
|
||||
<code>"duration": {"unit": "ms", "value": 5000}</code>.</p>
|
||||
<p>One particular implication of this rule is that money sums must <em>always</em> be accompanied by a currency code.</p>
|
||||
<p>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.</p>
|
||||
@ -1601,27 +1606,33 @@ str_replace(needle, replace, haystack)
|
||||
<p>— then developers will have to evaluate the flag <code>!beans_absence && !cup_absence</code> which is equivalent to <code>!(beans_absence || cup_absence)</code> conditions, and in this transition, people tend to make mistakes. Avoiding double negations helps little, and regretfully only general advice could be given: avoid the situations when developers have to evaluate such flags.</p>
|
||||
<h5><a href="#chapter-11-paragraph-8" id="chapter-11-paragraph-8" class="anchor">8. Avoid implicit type conversion</a></h5>
|
||||
<p>This advice is opposite to the previous one, ironically. When developing APIs you frequently need to add a new optional field with a non-empty default value. For example:</p>
|
||||
<pre><code>POST /v1/orders
|
||||
{}
|
||||
→
|
||||
{ "contactless_delivery": true }
|
||||
<pre><code>const orderParams = {
|
||||
contactless_delivery: false
|
||||
};
|
||||
const order = api.createOrder(
|
||||
orderParams
|
||||
);
|
||||
</code></pre>
|
||||
<p>This new <code>contactless_delivery</code> option isn't required, but its default value is <code>true</code>. A question arises: how developers should discern explicit intention to abolish the option (<code>false</code>) from knowing not it exists (field isn't set). They have to write something like:</p>
|
||||
<pre><code>if (Type(
|
||||
order.contactless_delivery
|
||||
) == 'Boolean' &&
|
||||
order.contactless_delivery == false) {
|
||||
…
|
||||
<p>This new <code>contactless_delivery</code> option isn't required, but its default value is <code>true</code>. A question arises: how developers should discern explicit intention to abolish the option (<code>false</code>) from knowing not it exists (the field isn't set). They have to write something like:</p>
|
||||
<pre><code>if (
|
||||
Type(
|
||||
orderParams.contactless_delivery
|
||||
) == 'Boolean' &&
|
||||
orderParams
|
||||
.contactless_delivery == false) {
|
||||
…
|
||||
}
|
||||
</code></pre>
|
||||
<p>This practice makes the code more complicated, and it's quite easy to make mistakes, which will effectively treat the field in an opposite manner. The same could happen if some special values (i.e. <code>null</code> or <code>-1</code>) to denote value absence are used.</p>
|
||||
<p><strong>NB</strong>: this observation is not valid if both the platform and the protocol unambiguously support special tokens to reset a field to its default value with zero abstraction overhead. However, full and consistent support of this functionality rarely sees implementation. Arguably, the only example of such an API among those being popular nowadays is SQL: the language has the <code>NULL</code> concept, and default field values functionality, and the support for operations like <code>UPDATE … SET field = DEFAULT</code> (in most dialects). Though working with the protocol is still complicated (for example, in many dialects there is no simple method of getting back those values reset by an <code>UPDATE … DEFAULT</code> query), SQL features working with defaults conveniently enough to use this functionality as is.</p>
|
||||
<p>If the protocol does not support resetting to default values as a first-class citizen, the universal rule is to make all new Boolean flags false by default.</p>
|
||||
<p><strong>Better</strong></p>
|
||||
<pre><code>POST /v1/orders
|
||||
{}
|
||||
→
|
||||
{ "force_contact_delivery": false }
|
||||
<pre><code>const orderParams = {
|
||||
force_contact_delivery: true
|
||||
};
|
||||
const order = api.createOrder(
|
||||
orderParams
|
||||
);
|
||||
</code></pre>
|
||||
<p>If a non-Boolean field with specially treated value absence is to be introduced, then introduce two fields.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
@ -2039,11 +2050,9 @@ GET /v1/record-views/{id}⮠
|
||||
</code></pre>
|
||||
<p>Since the produced view is immutable, access to it might be organized in any form, including a limit-offset scheme, cursors, <code>Range</code> header, etc. However, there is a downside: records modified after the view was generated will be misplaced or outdated.</p>
|
||||
<p><strong>Option two</strong>: guarantee a strict records order, for example, by introducing a concept of record change events:</p>
|
||||
<pre><code>POST /v1/records/modified/list
|
||||
{
|
||||
// Optional
|
||||
"cursor"
|
||||
}
|
||||
<pre><code>// `cursor` is optional
|
||||
GET /v1/records/modified/list⮠
|
||||
?[cursor={cursor}]
|
||||
→
|
||||
{
|
||||
"modified": [
|
||||
@ -2080,7 +2089,8 @@ POST /v1/orders/drafts
|
||||
{ "draft_id" }
|
||||
</code></pre>
|
||||
<pre><code>// Confirms the draft
|
||||
PUT /v1/orders/drafts/{draft_id}
|
||||
PUT /v1/orders/drafts⮠
|
||||
/{draft_id}/confirmation
|
||||
{ "confirmed": true }
|
||||
</code></pre>
|
||||
<p>Creating order drafts is a non-binding operation since it doesn't entail any consequences, so it's fine to create drafts without the idempotency token.</p>
|
||||
@ -2129,7 +2139,7 @@ X-Idempotency-Token: <token>
|
||||
<p>There is a common problem with implementing the changes list approach: what to do if some changes were successfully applied, while others are not? The rule is simple: if you may ensure the atomicity (e.g. either apply all changes or none of them) — do it.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>// Returns a list of recipes
|
||||
GET /v1/recipes
|
||||
api.getRecipes();
|
||||
→
|
||||
{
|
||||
"recipes": [{
|
||||
@ -2141,8 +2151,7 @@ GET /v1/recipes
|
||||
}]
|
||||
}
|
||||
// Changes recipes' parameters
|
||||
PATCH /v1/recipes
|
||||
{
|
||||
api.updateRecipes({
|
||||
"changes": [{
|
||||
"id": "lungo",
|
||||
"volume": "300ml"
|
||||
@ -2150,10 +2159,10 @@ PATCH /v1/recipes
|
||||
"id": "latte",
|
||||
"volume": "-1ml"
|
||||
}]
|
||||
}
|
||||
→ 400 Bad Request
|
||||
});
|
||||
→ Bad Request
|
||||
// Re-reading the list
|
||||
GET /v1/recipes
|
||||
api.getRecipes();
|
||||
→
|
||||
{
|
||||
"recipes": [{
|
||||
@ -2170,8 +2179,7 @@ GET /v1/recipes
|
||||
<p>— there is no way how the client might learn that failed operation was actually partially applied. Even if there is an indication of this fact in the response, the client still cannot tell, whether the lungo volume changed because of the request, or if some other client changed it.</p>
|
||||
<p>If you can't guarantee the atomicity of an operation, you should elaborate in detail on how to deal with it. There must be a separate status for each individual change.</p>
|
||||
<p><strong>Better</strong>:</p>
|
||||
<pre><code>PATCH /v1/recipes
|
||||
{
|
||||
<pre><code>api.updateRecipes({
|
||||
"changes": [{
|
||||
"recipe_id": "lungo",
|
||||
"volume": "300ml"
|
||||
@ -2179,11 +2187,11 @@ GET /v1/recipes
|
||||
"recipe_id": "latte",
|
||||
"volume": "-1ml"
|
||||
}]
|
||||
}
|
||||
});
|
||||
// You may actually return
|
||||
// a ‘partial success’ status
|
||||
// if the protocol allows it
|
||||
→ 200 OK
|
||||
→
|
||||
{
|
||||
"changes": [{
|
||||
"change_id",
|
||||
@ -2211,8 +2219,7 @@ GET /v1/recipes
|
||||
<li>expose a separate <code>/changes-history</code> endpoint for clients to get the history of applied changes even if the app crashed while getting a partial success response or there was a network timeout.</li>
|
||||
</ul>
|
||||
<p>Non-atomic changes are undesirable because they erode the idempotency concept. Let's take a look at the example:</p>
|
||||
<pre><code>PATCH /v1/recipes
|
||||
{
|
||||
<pre><code>api.updateRecipes({
|
||||
"idempotency_token",
|
||||
"changes": [{
|
||||
"recipe_id": "lungo",
|
||||
@ -2221,8 +2228,8 @@ GET /v1/recipes
|
||||
"recipe_id": "latte",
|
||||
"volume": "400ml"
|
||||
}]
|
||||
}
|
||||
→ 200 OK
|
||||
});
|
||||
→
|
||||
{
|
||||
"changes": [{
|
||||
…
|
||||
@ -2238,8 +2245,7 @@ GET /v1/recipes
|
||||
}
|
||||
</code></pre>
|
||||
<p>Imagine the client failed to get a response because of a network error, and it repeats the request:</p>
|
||||
<pre><code>PATCH /v1/recipes
|
||||
{
|
||||
<pre><code>api.updateRecipes({
|
||||
"idempotency_token",
|
||||
"changes": [{
|
||||
"recipe_id": "lungo",
|
||||
@ -2248,8 +2254,8 @@ GET /v1/recipes
|
||||
"recipe_id": "latte",
|
||||
"volume": "400ml"
|
||||
}]
|
||||
}
|
||||
→ 200 OK
|
||||
});
|
||||
→
|
||||
{
|
||||
"changes": [{
|
||||
…
|
||||
@ -2698,7 +2704,6 @@ POST /v1/runtimes/{id}/terminate
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<p>We will address these questions in more detail in the next chapters. Additionally, in Section III we will also discuss, how to communicate to customers about new releases and discontinued supporting of older versions, and how to stimulate them to adopt new API versions.</p>
|
||||
<h4>Simultaneous access to several API versions</h4>
|
||||
<p>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.</p>
|
||||
<p>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).</p>
|
||||
|
BIN
docs/API.en.pdf
BIN
docs/API.en.pdf
Binary file not shown.
BIN
docs/API.ru.docx
BIN
docs/API.ru.docx
Binary file not shown.
BIN
docs/API.ru.epub
BIN
docs/API.ru.epub
Binary file not shown.
@ -1487,24 +1487,26 @@ app.display(offers);
|
||||
<p>Из названия любой сущности должно быть очевидно, что она делает, и к каким побочным эффектам может привести её использование.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Отменяет заказ
|
||||
GET /orders/cancellation
|
||||
order.canceled = true;
|
||||
</code></pre>
|
||||
<p>Неочевидно, что достаточно просто обращения к сущности <code>cancellation</code> (что это?), тем более немодифицирующим методом <code>GET</code>, чтобы отменить заказ.</p>
|
||||
<p>Неочевидно, что поле состояния можно перезаписывать, и что это действие отменяет заказ.</p>
|
||||
<p><strong>Хорошо</strong>:</p>
|
||||
<pre><code>// Отменяет заказ
|
||||
POST /orders/cancel
|
||||
order.cancel();
|
||||
</code></pre>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Возвращает агрегированную
|
||||
// статистику заказов за всё время
|
||||
GET /orders/statistics
|
||||
orders.getStats()
|
||||
</code></pre>
|
||||
<p>Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы.</p>
|
||||
<p><strong>Хорошо</strong>:</p>
|
||||
<pre><code>// Возвращает агрегированную
|
||||
<pre><code>// Вычисляет и возвращает агрегированную
|
||||
// статистику заказов за указанный период
|
||||
POST /v1/orders/statistics/aggregate
|
||||
{ "begin_date", "end_date" }
|
||||
orders.calculateAggregatedStats({
|
||||
begin_date: <начало периода>
|
||||
end_date: <конец_периода>
|
||||
});
|
||||
</code></pre>
|
||||
<p><strong>Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает</strong>. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию.</p>
|
||||
<p>Два важных следствия:</p>
|
||||
@ -1518,7 +1520,9 @@ POST /v1/orders/statistics/aggregate
|
||||
<p><strong>Хорошо</strong>:<br>
|
||||
<code>"duration_ms": 5000</code><br>
|
||||
либо<br>
|
||||
<code>"duration": "5000ms"</code><br>
|
||||
<code>"duration": "5000ms"</code>
|
||||
либо
|
||||
<code>"iso_duration": "PT5S"</code>
|
||||
либо</p>
|
||||
<pre><code>"duration": {
|
||||
"unit": "ms",
|
||||
@ -1617,16 +1621,20 @@ str_replace(needle, replace, haystack)
|
||||
<p>— то разработчику потребуется вычислить флаг <code>!beans_absence && !cup_absence</code>, что эквивалентно <code>!(beans_absence || cup_absence)</code>, а вот в этом переходе ошибиться очень легко, и избегание двойных отрицаний помогает слабо. Здесь, к сожалению, есть только общий совет «избегайте ситуаций, когда разработчику нужно вычислять такие флаги».</p>
|
||||
<h5><a href="#chapter-11-paragraph-8" id="chapter-11-paragraph-8" class="anchor">8. Избегайте неявного приведения типов</a></h5>
|
||||
<p>Этот совет парадоксально противоположен предыдущему. Часто при разработке API возникает ситуация, когда добавляется новое необязательное поле с непустым значением по умолчанию. Например:</p>
|
||||
<pre><code>POST /v1/orders
|
||||
{ … }
|
||||
→
|
||||
{ "contactless_delivery": true }
|
||||
<pre><code>const orderParams = {
|
||||
contactless_delivery: false
|
||||
};
|
||||
const order = api.createOrder(
|
||||
orderParams
|
||||
);
|
||||
</code></pre>
|
||||
<p>Новая опция <code>contactless_delivery</code> является необязательной, однако её значение по умолчанию — <code>true</code>. Возникает вопрос, каким образом разработчик должен отличить явное <em>нежелание</em> пользоваться опцией (<code>false</code>) от незнания о её существовании (поле не задано). Приходится писать что-то типа такого:</p>
|
||||
<pre><code>if (Type(
|
||||
order.contactless_delivery
|
||||
) == 'Boolean' &&
|
||||
order.contactless_delivery == false) {
|
||||
<pre><code>if (
|
||||
Type(
|
||||
orderParams.contactless_delivery
|
||||
) == 'Boolean' &&
|
||||
orderParams
|
||||
.contactless_delivery == false) {
|
||||
…
|
||||
}
|
||||
</code></pre>
|
||||
@ -1634,10 +1642,12 @@ str_replace(needle, replace, haystack)
|
||||
<p><strong>NB</strong>. Это замечание не распространяется на те случаи, когда платформа и протокол однозначно и без всяких дополнительных абстракций поддерживают такие специальные значения для сброса значения поля в значение по умолчанию. Однако полная и консистентная поддержка частичных операций со сбросом значений полей практически нигде не имплементирована. Пожалуй, единственный пример такого API из имеющих широкое распространение сегодня — SQL: в языке есть и концепция <code>NULL</code>, и значения полей по умолчанию, и поддержка операций вида <code>UPDATE … SET field = DEFAULT</code> (в большинстве диалектов). Хотя работа с таким протоколом всё ещё затруднена (например, во многих диалектах нет простого способа получить обратно значение по умолчанию, которое выставил <code>UPDATE … DEFAULT</code>), логика работы с умолчаниями в SQL имплементирована достаточно хорошо, чтобы использовать её как есть.</p>
|
||||
<p>Если же протоколом явная работа со значениями по умолчанию не предусмотрена, универсальное правило — все новые необязательные булевы флаги должны иметь значение по умолчанию false.</p>
|
||||
<p><strong>Хорошо</strong></p>
|
||||
<pre><code>POST /v1/orders
|
||||
{}
|
||||
→
|
||||
{ "force_contact_delivery": false }
|
||||
<pre><code>const orderParams = {
|
||||
force_contact_delivery: true
|
||||
};
|
||||
const order = api.createOrder(
|
||||
orderParams
|
||||
);
|
||||
</code></pre>
|
||||
<p>Если же требуется ввести небулево поле, отсутствие которого трактуется специальным образом, то следует ввести пару полей.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
@ -2058,11 +2068,9 @@ GET /v1/record-views/{id}⮠
|
||||
</code></pre>
|
||||
<p>Поскольку созданное представление уже неизменяемо, доступ к нему можно организовать как угодно: через курсор, limit/offset, заголовок Range и т.д. Однако надо иметь в виду, что при переборе таких списков порядок может быть нарушен: записи, изменённые уже после генерации представления, будут находиться не на своих местах (либо быть неактуальны, если запись копируется целиком).</p>
|
||||
<p><strong>Вариант 2</strong>: гарантировать строгий неизменяемый порядок записей, например, путём введения понятия события изменения записи:</p>
|
||||
<pre><code>POST /v1/records/modified/list
|
||||
{
|
||||
// Опционально
|
||||
"cursor"
|
||||
}
|
||||
<pre><code>// Курсор опционален
|
||||
GET /v1/records/modified/list⮠
|
||||
?[cursor={cursor}]
|
||||
→
|
||||
{
|
||||
"modified": [
|
||||
@ -2099,7 +2107,8 @@ POST /v1/orders/drafts
|
||||
{ "draft_id" }
|
||||
</code></pre>
|
||||
<pre><code>// Подтверждает черновик заказа
|
||||
PUT /v1/orders/drafts/{draft_id}
|
||||
PUT /v1/orders/drafts⮠
|
||||
/{draft_id}/confirmation
|
||||
{ "confirmed": true }
|
||||
</code></pre>
|
||||
<p>Создание черновика заказа — необязывающая операция, которая не приводит ни к каким последствиям, поэтому допустимо создавать черновики без токена идемпотентности.
|
||||
@ -2148,7 +2157,7 @@ X-Idempotency-Token: <токен>
|
||||
<p>С применением массива изменений часто возникает вопрос: что делать, если часть изменений удалось применить, а часть — нет? Здесь правило очень простое: если вы можете обеспечить атомарность, т.е. выполнить либо все изменения сразу, либо ни одно из них — сделайте это.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Возвращает список рецептов
|
||||
GET /v1/recipes
|
||||
api.getRecipes();
|
||||
→
|
||||
{
|
||||
"recipes": [{
|
||||
@ -2161,8 +2170,7 @@ GET /v1/recipes
|
||||
}
|
||||
|
||||
// Изменяет параметры
|
||||
PATCH /v1/recipes
|
||||
{
|
||||
api.updateRecipes({
|
||||
"changes": [{
|
||||
"id": "lungo",
|
||||
"volume": "300ml"
|
||||
@ -2170,11 +2178,12 @@ PATCH /v1/recipes
|
||||
"id": "latte",
|
||||
"volume": "-1ml"
|
||||
}]
|
||||
}
|
||||
→ 400 Bad Request
|
||||
});
|
||||
→
|
||||
Bad Request
|
||||
|
||||
// Перечитываем список
|
||||
GET /v1/recipes
|
||||
api.getRecipes();
|
||||
→
|
||||
{
|
||||
"recipes": [{
|
||||
@ -2191,8 +2200,7 @@ GET /v1/recipes
|
||||
<p>— клиент никак не может узнать, что операция, которую он посчитал ошибочной, на самом деле частично применена. Даже если индицировать это в ответе, у клиента нет способа понять — значение объёма лунго изменилось вследствие запроса, или это конкурирующее изменение, выполненное другим клиентом.</p>
|
||||
<p>Если способа обеспечить атомарность выполнения операции нет, следует очень хорошо подумать над её обработкой. Следует предоставить способ получения статуса каждого изменения отдельно.</p>
|
||||
<p><strong>Лучше</strong>:</p>
|
||||
<pre><code>PATCH /v1/recipes
|
||||
{
|
||||
<pre><code>api.updateRecipes({
|
||||
"changes": [{
|
||||
"recipe_id": "lungo",
|
||||
"volume": "300ml"
|
||||
@ -2200,11 +2208,11 @@ GET /v1/recipes
|
||||
"recipe_id": "latte",
|
||||
"volume": "-1ml"
|
||||
}]
|
||||
}
|
||||
});
|
||||
// Можно воспользоваться статусом
|
||||
// «частичного успеха»,
|
||||
// если он предусмотрен протоколом
|
||||
→ 200 OK
|
||||
→
|
||||
{
|
||||
"changes": [{
|
||||
"change_id",
|
||||
@ -2232,8 +2240,7 @@ GET /v1/recipes
|
||||
<li>предоставить отдельный эндпойнт <code>/changes-history</code>, чтобы клиент мог получить информацию о выполненных изменениях, если во время обработки запроса произошла сетевая ошибка или приложение перезагрузилось.</li>
|
||||
</ul>
|
||||
<p>Неатомарные изменения нежелательны ещё и потому, что вносят неопределённость в понятие идемпотентности, даже если каждое вложенное изменение идемпотентно. Рассмотрим такой пример:</p>
|
||||
<pre><code>PATCH /v1/recipes
|
||||
{
|
||||
<pre><code>api.updateRecipes({
|
||||
"idempotency_token",
|
||||
"changes": [{
|
||||
"recipe_id": "lungo",
|
||||
@ -2242,8 +2249,8 @@ GET /v1/recipes
|
||||
"recipe_id": "latte",
|
||||
"volume": "400ml"
|
||||
}]
|
||||
}
|
||||
→ 200 OK
|
||||
});
|
||||
→
|
||||
{
|
||||
"changes": [{
|
||||
…
|
||||
@ -2259,8 +2266,7 @@ GET /v1/recipes
|
||||
}
|
||||
</code></pre>
|
||||
<p>Допустим, клиент не смог получить ответ и повторил запрос с тем же токеном идемпотентности.</p>
|
||||
<pre><code>PATCH /v1/recipes
|
||||
{
|
||||
<pre><code>api.updateRecipes({
|
||||
"idempotency_token",
|
||||
"changes": [{
|
||||
"recipe_id": "lungo",
|
||||
@ -2269,8 +2275,8 @@ GET /v1/recipes
|
||||
"recipe_id": "latte",
|
||||
"volume": "400ml"
|
||||
}]
|
||||
}
|
||||
→ 200 OK
|
||||
});
|
||||
→
|
||||
{
|
||||
"changes": [{
|
||||
…
|
||||
@ -2720,7 +2726,6 @@ POST /v1/runtimes/{id}/terminate
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<p>Дополнительно в разделе III мы также обсудим, каким образом предупреждать потребителей о выходе новых версий и прекращении поддержки старых, и как стимулировать их переходить на новые версии API.</p>
|
||||
<h4>Одновременный доступ к нескольким минорным версиям API</h4>
|
||||
<p>В современной промышленной разработке, особенно если мы говорим о внутренних API, новая версия, как правило, полностью заменяет предыдущую. Если в новой версии обнаруживаются критические ошибки, она может быть откачена (путём релиза предыдущей версии), но одновременно две сборки не сосуществуют. В случае публичных API такой подход становится тем более опасным, чем больше партнёров используют API.</p>
|
||||
<p>В самом деле, с ростом количества потребителей подход «откатить проблемную версию API в случае массовых жалоб» становится всё более деструктивным. Для партнёров, вообще говоря, оптимальным вариантом является жёсткая фиксация той версии API, для которой функциональность приложения была протестирована (и чтобы поставщик API при этом как-то незаметно исправлял возможные проблемы с информационной безопасностью и приводил своё ПО в соответствие с вновь возникающими законами).</p>
|
||||
|
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
Reference in New Issue
Block a user