1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-01-05 10:20:22 +02:00

clarifications, style fixes, typos

This commit is contained in:
Sergey Konstantinov 2021-01-04 02:32:59 +03:00
parent c63257eac3
commit 397b5d3557
4 changed files with 129 additions and 38 deletions

View File

@ -170,7 +170,7 @@ We're leaving the exercise of making these signatures better to the reader.
##### Use globally unique identifiers
It's considered good form to use globally unique strings as entity identifiers, either semantic (i.e. "lungo" for beverage types) or random ones (i.e. [UUID-4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)). It might turn out extremely useful should you merge data from several sources under single identifier.
It's considered good form to use globally unique strings as entity identifiers, either semantic (i.e. "lungo" for beverage types) or random ones (i.e. [UUID-4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random))). It might turn out to be extremely useful if you need to merge data from several sources under single identifier.
In general, we tend to advice using urn-like identifiers, e.g. `urn:order:<uuid>` (or just `order:<uuid>`). That helps a lot when dealing with legacy systems with different identifiers attached to the same entity. Namespaces in urns help to understand quickly which identifier is used, and is there a usage mistake.
@ -276,7 +276,20 @@ if (Type(order.contactless_delivery) == 'Boolean' &&
This practice makes the code more complicated, and it's quite easy to make mistakes, which will effectively treat the field in a quite opposite manner. Same could happen if some special values (i.e. `null` or `-1`) to denote value absence are used.
The universal rule to deal with such situations is to make all new Boolean flags being false by default. If a non-Boolean field with specially treated value absence is to be introduced, then introduce two fields.
The universal rule to deal with such situations is to make all new Boolean flags being false by default.
**Better**
```
POST /v1/orders
{}
{
"force_contact_delivery": false
}
```
If a non-Boolean field with specially treated value absence is to be introduced, then introduce two fields.
**Bad**:
```
@ -338,13 +351,25 @@ PATCH /v1/orders/123
{ "delivery_address" }
```
— this approach is usually taken to lessen request and response body size, plus it allows to implement collaborative editing cheaply. Both these advantages are imaginary.
— this approach is usually chosen to lessen request and response body sizes, plus it allows to implement collaborative editing cheaply. Both these advantages are imaginary.
In first, sparing bytes is seldom needed in modern apps. Network packets sizes (MTU, Maximum Transmission Unit) are more than a kilobyte right now; shortening responses is useless while they're less then a kilobyte. More viable approach would be separate endpoints to deal with large chunks of data and left all other endpoints data-rich.
**In first**, sparing bytes on semantic data is seldom needed in modern apps. Network packets sizes (MTU, Maximum Transmission Unit) are more than a kilobyte right now; shortening responses is useless while they're less then a kilobyte.
In seconds, shortening response sizes will backfire exactly with implementing collaborative editing: one client won't see the changes the other client have made. Generally speaking, in 9 cases out of 10 it is better to return a full entity state from any modifying operation, sharing the format with read access endpoint. Actually, you should always do this unless response size affects performance.
Excessive network traffic usually occurs if:
In third, this approach might work if you need to rewrite a field's value. But how to unset the field, return its value to the default state? For example, how to *remove* `client_phone_number_ext`?
* no data pagination is provided;
* no limits on field values are set;
* binary data is transmitted (graphics, audio, video, etc.)
Transferring only a subset of fields solves none of these cases, in the best case just masks it. More viable approach comprise:
* making separate endpoints for ‘heavy’ data;
* introducing pagination and field value length limits;
* stopping saving bytes in all other cases.
**In second**, shortening response sizes will backfire exactly with implementing collaborative editing: one client won't see the changes the other client have made. Generally speaking, in 9 cases out of 10 it is better to return a full entity state from any modifying operation, sharing the format with read access endpoint. Actually, you should always do this unless response size affects performance.
**In third**, this approach might work if you need to rewrite a field's value. But how to unset the field, return its value to the default state? For example, how to *remove* `client_phone_number_ext`?
In such cases special values are often being used, like `null`. But as we discussed above, this is a defective practice. Another variant is prohibiting non-required fields, but that would pose considerable obstacles in a way of expanding the API.
@ -849,7 +874,7 @@ It is also a good practice to return all detectable errors at once to spare deve
##### Maintain a proper error sequence
In first, always return unresolvable errors before re resolvable once:
**In first**, always return unresolvable errors before re resolvable once:
```
POST /v1/orders
@ -875,7 +900,7 @@ POST /v1/orders
```
— what was the point of renewing the offer if the order cannot be created anyway?
In second, maintain such a sequence of unresolvable errors which leads to a minimal amount of customers' and developers' irritation.
**In second**, maintain such a sequence of unresolvable errors which leads to a minimal amount of customers' and developers' irritation.
**Bad**:
```
@ -904,7 +929,7 @@ POST /v1/orders
```
— what was the point of showing the price changed dialog, if the user still can't make an order, even if the price is right? When one of the concurrent orders finishes, and the user is able to commit another one, prices, items availability, and other order parameters will likely need another correction.
In third, draw a chart: which error resolution might lead to the emergence of another one. Otherwise you might eventually return the same error several time, or worse, make a cycle of errors.
**In third**, draw a chart: which error resolution might lead to the emergence of another one. Otherwise you might eventually return the same error several time, or worse, make a cycle of errors.
```
// Create an order

View File

@ -1,12 +1,34 @@
<div class="cover"><h1>Sergey Konstantinov<br />The API</h1></div>
<div class="cover">
<h1>Sergey Konstantinov<br />The API</h1>
</div>
<div class="page-break" />
<div class="annotation">
<p>
<strong>Sergey Konstantinov. The API.</strong><br />
<a href="mailto:twirl-team@yandex.ru">twirl-team@yandex.ru</a>
&nbsp;&middot;
<a href="https://www.linkedin.com/in/twirl/"
>https://www.linkedin.com/in/twirl/</a
>
</p>
<p>Illustrations by Maria Konstantinova</p>
<img class="cc-by-nc-img" src="https://i.creativecommons.org/l/by-nc/4.0/88x31.png" />
<p class="cc-by-nc">
This work is licensed under a
<a href="http://creativecommons.org/licenses/by-nc/4.0/"
>Creative Commons Attribution-NonCommercial 4.0 International License</a
>.
</p>
<img
class="cc-by-nc-img"
src="https://i.creativecommons.org/l/by-nc/4.0/88x31.png"
/>
<p class="cc-by-nc">
This work is licensed under a
<a href="http://creativecommons.org/licenses/by-nc/4.0/"
>Creative Commons Attribution-NonCommercial 4.0 International
License</a
>.
</p>
<p>
Source codes are available on
<a href="https://github.com/twirl/The-API-Book">GitHub</a>
</p>
</div>
<a
href="https://github.com/twirl/The-API-Book/"
@ -37,5 +59,5 @@
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor"
class="octo-body"
></path></svg></a
>
></path></svg
></a>

View File

@ -165,7 +165,7 @@ str_replace(needle, replace, haystack)
##### Используйте глобально уникальные идентификаторы
Хороший тон при разработке API — использовать для идентификаторов сущностей глобально уникальные строки, либо семантичные (например, "lungo" для видов напитков), либо случайные (например [UUID-4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)). Это может чрезвычайно пригодиться, если вдруг придётся объединять данные из нескольких источников под одним идентификатором.
Хороший тон при разработке API — использовать для идентификаторов сущностей глобально уникальные строки, либо семантичные (например, "lungo" для видов напитков), либо случайные (например [UUID-4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random))). Это может чрезвычайно пригодиться, если вдруг придётся объединять данные из нескольких источников под одним идентификатором.
Мы вообще склонны порекомендовать использовать идентификаторы в urn-подобном формате, т.е. `urn:order:<uuid>` (или просто `order:<uuid>`), это сильно помогает с отладкой legacy-систем, где по историческим причинам есть несколько разных идентификаторов для одной и той же сущности — тогда неймспейсы в urn помогут быстро понять, что это за идентификатор и нет ли здесь ошибки использования.
@ -271,7 +271,19 @@ if (Type(order.contactless_delivery) == 'Boolean' &&
Эта практика ведёт к усложнению кода, который пишут разработчики, и в этом коде легко допустить ошибку, которая по сути меняет значение поля на противоположное. То же самое произойдёт, если для индикации отсутствия значения поля использовать специальное значение типа `null` или `-1`.
В этих ситуациях универсальное правило — все новые необязательные булевы флаги должны иметь значение по умолчанию false; если же требуется ввести небулево поле, отсутствие которого трактуется специальным образом, — следует ввести пару полей.
В этих ситуациях универсальное правило — все новые необязательные булевы флаги должны иметь значение по умолчанию false.
**Хорошо**
```
POST /v1/orders
{}
{
"force_contact_delivery": false
}
```
Если же требуется ввести небулево поле, отсутствие которого трактуется специальным образом, то следует ввести пару полей.
**Плохо**:
```
@ -311,7 +323,7 @@ POST /users
}
```
**NB**: противоречие с предыдущим советом в том, что мы специально ввели отрицающий флаг («нет лимита»), который по правилу двойных отрицаний пришлось переименовать в `abolish_spendings_limit`. Хотя это и хорошее название для отрицательного флага, семантика его довольно неочевидна, разработчикам придётся как минимум покопаться в документации. Таков путь.
**NB**: противоречие с предыдущим советом состоит в том, что мы специально ввели отрицающий флаг («нет лимита»), который по правилу двойных отрицаний пришлось переименовать в `abolish_spendings_limit`. Хотя это и хорошее название для отрицательного флага, семантика его довольно неочевидна, разработчикам придётся как минимум покопаться в документации. Таков путь.
##### Избегайте частичных обновлений
@ -336,11 +348,23 @@ PATCH /v1/orders/123
```
— такой подход часто практикуют для того, чтобы уменьшить объёмы запросов и ответов, плюс это позволяет дёшево реализовать совместное редактирование. Оба этих преимущества на самом деле являются мнимыми.
Во-первых, экономия объёма ответа в современных условиях требуется крайне редко. Максимальные размеры сетевых пакетов (MTU, Maximum Transmission Unit) в настоящее время составляют более килобайта; пытаться экономить на размере ответа, пока он не превышает килобайт — попросту бессмысленная трата времени. Да и в целом, скорее более оправдан следующий подход: для тяжёлых данных следует сделать отдельный эндпойнт, а на всём остальном не пытаться экономить.
**Во-первых**, экономия объёма ответа в современных условиях требуется редко. Максимальные размеры сетевых пакетов (MTU, Maximum Transmission Unit) в настоящее время составляют более килобайта; пытаться экономить на размере ответа, пока он не превышает килобайт — попросту бессмысленная трата времени.
Во-вторых, экономия размера ответа выйдет боком как раз при совместном редактировании: один клиент не будет видеть, какие изменения внёс другой. Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа не оказывает значительного влияния на производительность) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.
Перерасход трафика возникает, если:
В-третьих, этот подход может как-то работать при необходимость перезаписать поле. Но что делать, если поле требуется сбросить к значению по умолчанию? Например, как *удалить* `client_phone_number_ext`?
* не предусмотрен постраничный перебор данных;
* не предусмотрены ограничения на размер значений полей;
* передаются бинарные данные (графика, аудио, видео и т.д.)
Во всех трёх случаях передача части полей в лучше случае замаскирует проблему, но не решит. Более оправдан следующий подход:
* для «тяжёлых» данных сделать отдельные эндпойнты;
* ввести пагинацию и лимитирование значений полей;
* на всём остальном не пытаться экономить.
**Во-вторых**, экономия размера ответа выйдет боком как раз при совместном редактировании: один клиент не будет видеть, какие изменения внёс другой. Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа не оказывает значительного влияния на производительность) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.
**В-третьих**, этот подход может как-то работать при необходимость перезаписать поле. Но что делать, если поле требуется сбросить к значению по умолчанию? Например, как *удалить* `client_phone_number_ext`?
Часто в таких случаях прибегают к специальным значениям, которые означают удаление поля, например, null. Но, как мы разобрали выше, это плохая практика. Другой вариант — запрет необязательных полей, но это существенно усложняет дальнейшее развитие API.
@ -846,7 +870,7 @@ POST /v1/coffee-machines/search
##### Соблюдайте правильный порядок ошибок
Во-первых, всегда показывайте неразрешимые ошибки прежде разрешимых:
**Во-первых**, всегда показывайте неразрешимые ошибки прежде разрешимых:
```
POST /v1/orders
{
@ -871,7 +895,7 @@ POST /v1/orders
```
— какой был смысл получать новый `offer`, если заказ все равно не может быть создан?
Во-вторых, соблюдайте такой порядок разрешимых ошибок, который приводит к наименьшему раздражению пользователя и разработчика. В частности, следует начинать с более значимых ошибок, решение которых требует более глобальных изменений.
**Во-вторых**, соблюдайте такой порядок разрешимых ошибок, который приводит к наименьшему раздражению пользователя и разработчика. В частности, следует начинать с более значимых ошибок, решение которых требует более глобальных изменений.
**Плохо**:
```
@ -900,7 +924,7 @@ POST /v1/orders
```
— какой был смысл показывать пользователю диалог об изменившейся цене, если и с правильной ценой заказ он сделать все равно не сможет? Пока один из его предыдущих заказов завершится и можно будет сделать следующий заказ, цену, наличие и другие параметры заказа всё равно придётся корректировать ещё раз.
В-третьих, постройте таблицу: разрешение какой ошибки может привести к появлению другой, иначе вы можете показать одну и ту же ошибку несколько раз, а то и вовсе зациклить разрешение ошибок.
**В-третьих**, постройте таблицу: разрешение какой ошибки может привести к появлению другой, иначе вы можете показать одну и ту же ошибку несколько раз, а то и вовсе зациклить разрешение ошибок.
```
// Создаём заказ с платной доставкой

View File

@ -1,14 +1,34 @@
<div class="cover"><h1>Сергей Константинов<br />API</h1></div>
<img class="cc-by-nc-img" src="https://i.creativecommons.org/l/by-nc/4.0/88x31.png" />
<p class="cc-by-nc">
Это произведение доступно по
<a href="http://creativecommons.org/licenses/by-nc/4.0/"
>лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция —
Некоммерческое использование») 4.0 Всемирная</a
>.
</p>
<div class="cover">
<h1>Сергей Константинов<br />API</h1>
</div>
<div class="page-break" />
<div class="annotation">
<p>
<strong>Сергей Константинов. API.</strong><br />
<a href="mailto:twirl-team@yandex.ru">twirl-team@yandex.ru</a>
&nbsp;&middot;
<a href="https://www.linkedin.com/in/twirl/"
>https://www.linkedin.com/in/twirl/</a
>
</p>
<p>Иллюстрации: Мария Константинова</p>
<img
class="cc-by-nc-img"
src="https://i.creativecommons.org/l/by-nc/4.0/88x31.png"
/>
<p class="cc-by-nc">
Это произведение доступно по
<a href="http://creativecommons.org/licenses/by-nc/4.0/"
>лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция —
Некоммерческое использование») 4.0 Всемирная</a
>.
</p>
<p>
Исходный код доступен на
<a href="https://github.com/twirl/The-API-Book">GitHub</a>
</p>
</div>
<a
href="https://github.com/twirl/The-API-Book/"
class="github-corner"