diff --git a/README.md b/README.md
index e2bde2b..b3581e6 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,30 @@
-# [Read ‘The API’ Book by Sergey Konstantinov](https://twirl.github.io/The-API-Book)
-# [Читать книгу ‘The API’ Сергея Константинова](https://twirl.github.io/The-API-Book/index.ru.html)
+# Read [‘The API’ Book by Sergey Konstantinov](https://twirl.github.io/The-API-Book) in English
+# Читать [книгу ‘The API’ Сергея Константинова](https://twirl.github.io/The-API-Book/index.ru.html) по-русски
This is the working repository for ‘The API’ book written by Sergey Konstantinov ([email](mailto:twirl-team@yandex.ru), [Linkedin profile](https://linkedin.com/in/twirl)).
## Current State and the Roadmap
-Right now all three section (‘The API Design’, ‘The Backwards Compatibility’, and ‘The API Product’) are finished. So the book is basically ready, I'm working on some cosmetics:
+Right now all three section (‘The API Design’, ‘The Backwards Compatibility’, and ‘The API Product’) are finished. So the book is basically ready. However, after some beta-testing I understood there were several important problems.
+ 1. 'Describing final interfaces' chapter is way too overloaded; many concepts explained there deserve a separate chapter, and being minimized to fit the format, they arise a lot of controversy.
+ 2. Though I've tried to avoid talking about any specific paradigm (REST in particular), it's often being read as such, thus igniting discussions on whether the samples are proper REST.
- * adding readable schemes where it's appropriate;
- * refactoring the ‘Describing Final Interfaces’ chapters;
- * rephrasing and expanding the chapters on versioning and identifying users.
+So the current plan is:
+ 1. To split Chapter 11 into a full Section III (work title: 'API Patterns') comprising:
+ * defining API-first approach in a technical sense;
+ * the review of API-describing paradigms (OpenAPI/REST, GraphQL, GRPC, JSON-RPC, SOAP);
+ * working with default values, backwards compatibility-wise;
+ * (a)synchronous interaction;
+ * strong and weak consistency;
+ * push and poll models;
+ * machine-readable APIs: iterable lists, cursors, observability;
+ * an amount of traffic and data compression;
+ * API errors: resolvability, reduction to defaults;
+ * degrading properly.
+ 2. To compile Section IV ‘HTTP API & JSON’ from current drafts + HTTP general knowledge + codestyle.
+ 3. Maybe, try to compile Section V ‘SDK’ (much harder as there are very few drafts).
-I also have more distant plans on adding two more subsections to Section I.
- * Section Ia ‘JSON HTTP APIs’:
- * the REST myth;
- * following HTTP spec, including those parts where you should not follow the spec;
- * best practices;
- * Section Ib ‘SDK Design’ covering more tricky issues of having proving UI alongside the API (no specific plan right now)
+Also, the book still lacks the readable schemes which I'm still planning to plot with mermaid.
## Translation
diff --git a/src/en/clean-copy/02-Section I. The API Design/05.md b/src/en/clean-copy/02-Section I. The API Design/05.md
index c699994..756e701 100644
--- a/src/en/clean-copy/02-Section I. The API Design/05.md
+++ b/src/en/clean-copy/02-Section I. The API Design/05.md
@@ -25,30 +25,33 @@ Entity name must explicitly tell what it does and what side effects to expect wh
**Bad**:
```
// Cancels an order
-GET /orders/cancellation
+order.canceled = true;
```
-It's quite a surprise that accessing the `cancellation` resource (what is it?) with the non-modifying `GET` method actually cancels an order.
+It's unobvious that a state field might be set, and that this operation will cancel the order.
**Better**:
```
// Cancels an order
-POST /orders/cancel
+order.cancel();
```
**Bad**:
```
// Returns aggregated statistics
// since the beginning of time
-GET /orders/statistics
+orders.getStats()
```
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.
**Better**:
```
-// Returns aggregated statistics
+// 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
+});
```
**Try to design function signatures to be absolutely transparent about what the function does, what arguments it takes, and what's the result**. While reading a code working with your API, it must be easy to understand what it does without reading docs.
@@ -76,6 +79,8 @@ So *always* specify exactly which standard is applied. Exceptions are possible i
or
`"duration": "5000ms"`
or
+ `"iso_duration": "PT5S"`
+ or
`"duration": {"unit": "ms", "value": 5000}`.
One particular implication of this rule is that money sums must *always* be accompanied by a currency code.
@@ -210,20 +215,24 @@ GET /coffee-machines/{id}/stocks
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:
```
-POST /v1/orders
-{}
-→
-{ "contactless_delivery": true }
+const orderParams = {
+ contactless_delivery: false
+};
+const order = api.createOrder(
+ orderParams
+);
```
-This new `contactless_delivery` option isn't required, but its default value is `true`. A question arises: how developers should discern explicit intention to abolish the option (`false`) from knowing not it exists (field isn't set). They have to write something like:
+This new `contactless_delivery` option isn't required, but its default value is `true`. A question arises: how developers should discern explicit intention to abolish the option (`false`) from knowing not it exists (the field isn't set). They have to write something like:
```
-if (Type(
- order.contactless_delivery
- ) == 'Boolean' &&
- order.contactless_delivery == false) {
- …
+if (
+ Type(
+ orderParams.contactless_delivery
+ ) == 'Boolean' &&
+ orderParams
+ .contactless_delivery == false) {
+ …
}
```
@@ -235,10 +244,12 @@ If the protocol does not support resetting to default values as a first-class ci
**Better**
```
-POST /v1/orders
-{}
-→
-{ "force_contact_delivery": false }
+const orderParams = {
+ force_contact_delivery: true
+};
+const order = api.createOrder(
+ orderParams
+);
```
If a non-Boolean field with specially treated value absence is to be introduced, then introduce two fields.
@@ -733,11 +744,9 @@ Since the produced view is immutable, access to it might be organized in any for
**Option two**: guarantee a strict records order, for example, by introducing a concept of record change events:
```
-POST /v1/records/modified/list
-{
- // Optional
- "cursor"
-}
+// `cursor` is optional
+GET /v1/records/modified/list⮠
+ ?[cursor={cursor}]
→
{
"modified": [
@@ -791,7 +800,8 @@ POST /v1/orders/drafts
```
```
// Confirms the draft
-PUT /v1/orders/drafts/{draft_id}
+PUT /v1/orders/drafts⮠
+ /{draft_id}/confirmation
{ "confirmed": true }
```
@@ -852,7 +862,7 @@ There is a common problem with implementing the changes list approach: what to d
**Bad**:
```
// Returns a list of recipes
-GET /v1/recipes
+api.getRecipes();
→
{
"recipes": [{
@@ -864,8 +874,7 @@ GET /v1/recipes
}]
}
// Changes recipes' parameters
-PATCH /v1/recipes
-{
+api.updateRecipes({
"changes": [{
"id": "lungo",
"volume": "300ml"
@@ -873,10 +882,10 @@ PATCH /v1/recipes
"id": "latte",
"volume": "-1ml"
}]
-}
-→ 400 Bad Request
+});
+→ Bad Request
// Re-reading the list
-GET /v1/recipes
+api.getRecipes();
→
{
"recipes": [{
@@ -896,8 +905,7 @@ If you can't guarantee the atomicity of an operation, you should elaborate in de
**Better**:
```
-PATCH /v1/recipes
-{
+api.updateRecipes({
"changes": [{
"recipe_id": "lungo",
"volume": "300ml"
@@ -905,11 +913,11 @@ PATCH /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",
@@ -938,8 +946,7 @@ Might be of use:
Non-atomic changes are undesirable because they erode the idempotency concept. Let's take a look at the example:
```
-PATCH /v1/recipes
-{
+api.updateRecipes({
"idempotency_token",
"changes": [{
"recipe_id": "lungo",
@@ -948,8 +955,8 @@ PATCH /v1/recipes
"recipe_id": "latte",
"volume": "400ml"
}]
-}
-→ 200 OK
+});
+→
{
"changes": [{
…
@@ -968,8 +975,7 @@ PATCH /v1/recipes
Imagine the client failed to get a response because of a network error, and it repeats the request:
```
-PATCH /v1/recipes
-{
+api.updateRecipes({
"idempotency_token",
"changes": [{
"recipe_id": "lungo",
@@ -978,8 +984,8 @@ PATCH /v1/recipes
"recipe_id": "latte",
"volume": "400ml"
}]
-}
-→ 200 OK
+});
+→
{
"changes": [{
…
diff --git a/src/ru/clean-copy/02-Раздел I. Проектирование API/05.md b/src/ru/clean-copy/02-Раздел I. Проектирование API/05.md
index 76721c6..1d81974 100644
--- a/src/ru/clean-copy/02-Раздел I. Проектирование API/05.md
+++ b/src/ru/clean-copy/02-Раздел I. Проектирование API/05.md
@@ -25,30 +25,32 @@
**Плохо**:
```
// Отменяет заказ
-GET /orders/cancellation
+order.canceled = true;
```
-Неочевидно, что достаточно просто обращения к сущности `cancellation` (что это?), тем более немодифицирующим методом `GET`, чтобы отменить заказ.
+Неочевидно, что поле состояния можно перезаписывать, и что это действие отменяет заказ.
**Хорошо**:
```
// Отменяет заказ
-POST /orders/cancel
+order.cancel();
```
**Плохо**:
```
// Возвращает агрегированную
// статистику заказов за всё время
-GET /orders/statistics
+orders.getStats()
```
Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы.
**Хорошо**:
```
-// Возвращает агрегированную
+// Вычисляет и возвращает агрегированную
// статистику заказов за указанный период
-POST /v1/orders/statistics/aggregate
-{ "begin_date", "end_date" }
+orders.calculateAggregatedStats({
+ begin_date: <начало периода>
+ end_date: <конец_периода>
+});
```
**Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает**. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию.
@@ -72,7 +74,9 @@ POST /v1/orders/statistics/aggregate
**Хорошо**:
`"duration_ms": 5000`
либо
- `"duration": "5000ms"`
+ `"duration": "5000ms"`
+ либо
+ `"iso_duration": "PT5S"`
либо
```
"duration": {
@@ -220,19 +224,23 @@ GET /coffee-machines/{id}/stocks
Этот совет парадоксально противоположен предыдущему. Часто при разработке API возникает ситуация, когда добавляется новое необязательное поле с непустым значением по умолчанию. Например:
```
-POST /v1/orders
-{ … }
-→
-{ "contactless_delivery": true }
+const orderParams = {
+ contactless_delivery: false
+};
+const order = api.createOrder(
+ orderParams
+);
```
Новая опция `contactless_delivery` является необязательной, однако её значение по умолчанию — `true`. Возникает вопрос, каким образом разработчик должен отличить явное *нежелание* пользоваться опцией (`false`) от незнания о её существовании (поле не задано). Приходится писать что-то типа такого:
```
-if (Type(
- order.contactless_delivery
- ) == 'Boolean' &&
- order.contactless_delivery == false) {
+if (
+ Type(
+ orderParams.contactless_delivery
+ ) == 'Boolean' &&
+ orderParams
+ .contactless_delivery == false) {
…
}
```
@@ -245,10 +253,12 @@ if (Type(
**Хорошо**
```
-POST /v1/orders
-{}
-→
-{ "force_contact_delivery": false }
+const orderParams = {
+ force_contact_delivery: true
+};
+const order = api.createOrder(
+ orderParams
+);
```
Если же требуется ввести небулево поле, отсутствие которого трактуется специальным образом, то следует ввести пару полей.
@@ -741,11 +751,9 @@ GET /v1/record-views/{id}⮠
**Вариант 2**: гарантировать строгий неизменяемый порядок записей, например, путём введения понятия события изменения записи:
```
-POST /v1/records/modified/list
-{
- // Опционально
- "cursor"
-}
+// Курсор опционален
+GET /v1/records/modified/list⮠
+ ?[cursor={cursor}]
→
{
"modified": [
@@ -799,7 +807,8 @@ POST /v1/orders/drafts
```
```
// Подтверждает черновик заказа
-PUT /v1/orders/drafts/{draft_id}
+PUT /v1/orders/drafts⮠
+ /{draft_id}/confirmation
{ "confirmed": true }
```
Создание черновика заказа — необязывающая операция, которая не приводит ни к каким последствиям, поэтому допустимо создавать черновики без токена идемпотентности.
@@ -859,7 +868,7 @@ X-Idempotency-Token: <токен>
**Плохо**:
```
// Возвращает список рецептов
-GET /v1/recipes
+api.getRecipes();
→
{
"recipes": [{
@@ -872,8 +881,7 @@ GET /v1/recipes
}
// Изменяет параметры
-PATCH /v1/recipes
-{
+api.updateRecipes({
"changes": [{
"id": "lungo",
"volume": "300ml"
@@ -881,11 +889,12 @@ PATCH /v1/recipes
"id": "latte",
"volume": "-1ml"
}]
-}
-→ 400 Bad Request
+});
+→
+Bad Request
// Перечитываем список
-GET /v1/recipes
+api.getRecipes();
→
{
"recipes": [{
@@ -905,8 +914,7 @@ GET /v1/recipes
**Лучше**:
```
-PATCH /v1/recipes
-{
+api.updateRecipes({
"changes": [{
"recipe_id": "lungo",
"volume": "300ml"
@@ -914,11 +922,11 @@ PATCH /v1/recipes
"recipe_id": "latte",
"volume": "-1ml"
}]
-}
+});
// Можно воспользоваться статусом
// «частичного успеха»,
// если он предусмотрен протоколом
-→ 200 OK
+→
{
"changes": [{
"change_id",
@@ -947,8 +955,7 @@ PATCH /v1/recipes
Неатомарные изменения нежелательны ещё и потому, что вносят неопределённость в понятие идемпотентности, даже если каждое вложенное изменение идемпотентно. Рассмотрим такой пример:
```
-PATCH /v1/recipes
-{
+api.updateRecipes({
"idempotency_token",
"changes": [{
"recipe_id": "lungo",
@@ -957,8 +964,8 @@ PATCH /v1/recipes
"recipe_id": "latte",
"volume": "400ml"
}]
-}
-→ 200 OK
+});
+→
{
"changes": [{
…
@@ -977,8 +984,7 @@ PATCH /v1/recipes
Допустим, клиент не смог получить ответ и повторил запрос с тем же токеном идемпотентности.
```
-PATCH /v1/recipes
-{
+api.updateRecipes({
"idempotency_token",
"changes": [{
"recipe_id": "lungo",
@@ -987,8 +993,8 @@ PATCH /v1/recipes
"recipe_id": "latte",
"volume": "400ml"
}]
-}
-→ 200 OK
+});
+→
{
"changes": [{
…
diff --git a/src/ru/drafts/02-Раздел I. Проектирование API/05.md b/src/ru/drafts/02-Раздел I. Проектирование API/05.md
new file mode 100644
index 0000000..a05fdfc
--- /dev/null
+++ b/src/ru/drafts/02-Раздел I. Проектирование API/05.md
@@ -0,0 +1,675 @@
+### Описание конечных интерфейсов
+
+Определив все сущности, их ответственность и отношения друг с другом, мы переходим непосредственно к разработке API: нам осталось прописать номенклатуру всех объектов, полей, методов и функций в деталях. В этой главе мы дадим сугубо практические советы, как сделать API удобным и понятным.
+
+Важнейшая задача разработчика API — добиться того, чтобы код, написанный поверх API другими разработчиками, легко читался и поддерживался. Помните, что закон больших чисел работает против вас: если какую-то концепцию или сигнатуру вызова можно понять неправильно, значит, её неизбежно будет понимать неправильно всё большее число партнеров по мере роста популярности API.
+
+**NB**: примеры, приведённые в этой главе, прежде всего иллюстрируют проблемы консистентности и читабельности, возникающие при разработке API. Мы не ставим здесь цели дать рекомендации по разработке REST API (такого рода советы будут даны в разделе III) или стандартных библиотек языков программирования — важен не конкретный синтаксис, а общая идея.
+
+Важное уточнение под номером ноль:
+
+##### 0. Правила не должны применяться бездумно
+
+Правило — это просто кратко сформулированное обобщение опыта. Они не действуют безусловно и не означают, что можно не думать головой. У каждого правила есть какая-то рациональная причина его существования. Если в вашей ситуации нет причин следовать правилу — значит, следовать ему не нужно.
+
+Например, требование консистентности номенклатуры существует затем, чтобы разработчик тратил меньше времени на чтение документации; если вам _необходимо_, чтобы разработчик обязательно прочитал документацию по какому-то методу, вполне разумно сделать его сигнатуру нарочито неконсистентно.
+
+Это соображение применимо ко всем принципам ниже. Если из-за следования правилам у вас получается неудобный, громоздкий, неочевидный API — это повод пересмотреть правила (или API).
+
+Важно понимать, что вы вольны вводить свои собственные конвенции. Например, в некоторых фреймворках сознательно отказываются от парных методов `set_entity` / `get_entity` в пользу одного метода `entity` с опциональным параметром. Важно только проявить последовательность в её применении — если такая конвенция вводится, то абсолютно все методы API должны иметь подобную полиморфную сигнатуру, или по крайней мере должен существовать принцип именования, отличающий такие комбинированные методы от обычных вызовов.
+
+##### Явное лучше неявного
+
+Из названия любой сущности должно быть очевидно, что она делает, и к каким побочным эффектам может привести её использование.
+
+**Плохо**:
+```
+// Отменяет заказ
+order.canceled = true;
+```
+Неочевидно, что поле состояния можно перезаписывать, и что это действие отменяет заказ.
+
+**Хорошо**:
+```
+// Отменяет заказ
+order.cancel();
+```
+
+**Плохо**:
+```
+// Возвращает агрегированную
+// статистику заказов за всё время
+orders.getStats()
+```
+Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы.
+
+**Хорошо**:
+```
+// Вычисляет и возвращает агрегированную
+// статистику заказов за указанный период
+orders.calculateAggregatedStats({
+ begin_date: <начало периода>
+ end_date: <конец_периода>
+});
+```
+
+**Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает**. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию.
+
+Два важных следствия:
+
+**1.1.** Если операция модифицирующая, это должно быть очевидно из сигнатуры. В частности, не может быть модифицирующих операций за `GET`.
+
+**1.2.** Если в номенклатуре вашего API есть как синхронные операции, так и асинхронные, то (а)синхронность должна быть очевидна из сигнатур, **либо** должна существовать конвенция именования, позволяющая отличать синхронные операции от асинхронных.
+
+##### Указывайте использованные стандарты
+
+К сожалению, человечество не в состоянии договориться о таких простейших вещах, как «с какого дня начинается неделя». Поэтому *всегда* указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе.
+
+**Плохо**: `"date": "11/12/2020"` — существует огромное количество стандартов записи дат, плюс из этой записи невозможно даже понять, что здесь число, а что месяц.
+
+**Хорошо**: `"iso_date": "2020-11-12"`.
+
+**Плохо**: `"duration": 5000` — пять тысяч чего?
+
+**Хорошо**:
+ `"duration_ms": 5000`
+ либо
+ `"duration": "5000ms"`
+ либо
+ `"iso_duration": "PT5S"`
+ либо
+```
+"duration": {
+ "unit": "ms",
+ "value": 5000
+}
+```
+
+Отдельное следствие из этого правила — денежные величины *всегда* должны сопровождаться указанием кода валюты.
+
+Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что, как ни сделай, — кто-то останется недовольным. Классический пример такого рода — порядок географических координат («широта-долгота» против «долгота-широта»). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе II.
+
+##### Сущности должны именоваться конкретно
+
+Избегайте одиночных слов-«амёб» без определённой семантики, таких как get, apply, make.
+
+**Плохо**: `user.get()` — неочевидно, что конкретно будет возвращено.
+
+**Хорошо**: `user.get_id()`.
+
+##### Не экономьте буквы
+
+В XXI веке давно уже нет нужды называть переменные покороче.
+
+**Плохо**: `order.time()` — неясно, о каком времени идёт речь: время создания заказа, время готовности заказа, время ожидания заказа?…
+
+**Хорошо**:
+```
+order
+ .get_estimated_delivery_time()
+```
+
+**Плохо**:
+```
+// возвращает положение
+// первого вхождения в строку str1
+// любого символа из строки str2
+strpbrk (str1, str2)
+```
+Возможно, автору этого API казалось, что аббревиатура `pbrk` что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк `str1`, `str2` является набором символов для поиска.
+
+**Хорошо**:
+```
+str_search_for_characters(
+ str,
+ lookup_character_set
+)
+```
+— однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение `string` до `str` выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.
+
+**NB**: иногда названия полей сокращают или вовсе опускают (например, возвращают массив разнородных объектов вместо набора именованных полей) в погоне за уменьшением количества трафика. В абсолютном большинстве случаев это бессмысленно, поскольку текстовые данные при передаче обычно дополнительно сжимают на уровне протокола.
+
+##### Тип поля должен быть ясен из его названия
+
+Если поле называется `recipe` — мы ожидаем, что его значением является сущность типа `Recipe`. Если поле называется `recipe_id` — мы ожидаем, что его значением является идентификатор, который мы сможем найти в составе сущности `Recipe`.
+
+То же касается и примитивных типов. Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — `objects`, `children`; если это невозможно (термин неисчисляем), следует добавить префикс или постфикс, не оставляющий сомнений.
+
+**Плохо**: `GET /news` — неясно, будет ли получена какая-то конкретная новость или массив новостей.
+
+**Хорошо**: `GET /news-list`.
+
+Аналогично, если ожидается булево значение, то это должно быть очевидно из названия, т.е. именование должно описывать некоторое качественное состояние, например, `is_ready`, `open_now`.
+
+**Плохо**: `"task.status": true` — неочевидно, что статус бинарен, к тому же такой API будет нерасширяемым.
+
+**Хорошо**: `"task.is_finished": true`.
+
+Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учётом специфики first-class citizen-типов. Например, в JSON не существует объектов типа `Date`, и даты приходится передавать в виде числа или строки; разумно такие даты индицировать с помощью, например, постфикса `_at` (`created_at`, `occurred_at` и т.д.) или `_date`.
+
+Если наименование сущности само по себе является каким-либо термином, способным смутить разработчика, лучше добавить лишний префикс или постфикс во избежание непонимания.
+
+**Плохо**:
+```
+// Возвращает список
+// встроенных функций кофемашины
+GET /coffee-machines/{id}/functions
+```
+Слово "functions" многозначное: оно может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).
+
+**Хорошо**:
+```
+GET /v1/coffee-machines/{id}⮠
+ /builtin-functions-list
+```
+
+##### Подобные сущности должны называться подобно и вести себя подобным образом
+
+**Плохо**: `begin_transition` / `stop_transition`
+— `begin` и `stop` — непарные термины; разработчик будет вынужден рыться в документации.
+
+**Хорошо**: `begin_transition` / `end_transition` либо `start_transition` / `stop_transition`.
+
+**Плохо**:
+```
+// Находит первую позицию строки `needle`
+// внутри строки `haystack`
+strpos(haystack, needle)
+```
+```
+// Находит и заменяет
+// все вхождения строки `needle`
+// внутри строки `haystack`
+// на строку `replace`
+str_replace(needle, replace, haystack)
+```
+Здесь нарушены сразу несколько правил:
+ * написание неконсистентно в части знака подчёркивания;
+ * близкие по смыслу методы имеют разный порядок аргументов `needle`/`haystack`;
+ * первый из методов находит только первое вхождение строки `needle`, а другой — все вхождения, и об этом поведении никак нельзя узнать из сигнатуры функций.
+
+Упражнение «как сделать эти интерфейсы хорошо» предоставим читателю.
+
+##### Избегайте двойных отрицаний
+
+**Плохо**: `"dont_call_me": false`
+— люди в целом плохо считывают двойные отрицания. Это провоцирует ошибки.
+
+**Лучше**: `"prohibit_calling": true` или `"avoid_calling": true`
+— читается лучше, хотя обольщаться всё равно не следует. Насколько это возможно откажитесь от семантически двойных отрицаний, даже если вы придумали «негативное» слово без явной приставки «не».
+
+Стоит также отметить, что в использовании [законов де Моргана](https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BA%D0%BE%D0%BD%D1%8B_%D0%B4%D0%B5_%D0%9C%D0%BE%D1%80%D0%B3%D0%B0%D0%BD%D0%B0) ошибиться ещё проще, чем в двойных отрицаниях. Предположим, что у вас есть два флага:
+
+```
+GET /coffee-machines/{id}/stocks
+→
+{
+ "has_beans": true,
+ "has_cup": true
+}
+```
+
+Условие «кофе можно приготовить» будет выглядеть как `has_beans && has_cup` — есть и зерно, и стакан. Однако, если по какой-то причине в ответе будут отрицания тех же флагов:
+
+```
+{
+ "beans_absence": false,
+ "cup_absence": false
+}
+```
+— то разработчику потребуется вычислить флаг `!beans_absence && !cup_absence`, что эквивалентно `!(beans_absence || cup_absence)`, а вот в этом переходе ошибиться очень легко, и избегание двойных отрицаний помогает слабо. Здесь, к сожалению, есть только общий совет «избегайте ситуаций, когда разработчику нужно вычислять такие флаги».
+
+##### Отсутствие результата — тоже результат
+
+Если сервер корректно обработал вопрос и никакой внештатной ситуации не возникло — следовательно, это не ошибка. К сожалению, весьма распространён антипаттерн, когда отсутствие результата считается ошибкой.
+
+**Плохо**
+```
+POST /v1/coffee-machines/search
+{
+ "query": "lungo",
+ "location": <положение пользователя>
+}
+→ 404 Not Found
+{
+ "localized_message":
+ "Рядом с вами не делают лунго"
+}
+```
+
+Статусы `4xx` означают, что клиент допустил ошибку; однако в данном случае никакой ошибки сделано не было ни пользователем, ни разработчиком: клиент же не может знать заранее, готовят здесь лунго или нет.
+
+**Хорошо**:
+```
+POST /v1/coffee-machines/search
+{
+ "query": "lungo",
+ "location": <положение пользователя>
+}
+→ 200 OK
+{
+ "results": []
+}
+```
+
+Это правило вообще можно упростить до следующего: если результатом операции является массив данных, то пустота этого массива — не ошибка, а штатный ответ. (Если, конечно, он допустим по смыслу; пустой массив координат, например, является ошибкой.)
+
+##### Валидируйте корректность операции
+
+В ситуации выбора: указать на ошибку или молча её проглотить — разработчик API должен всегда выбирать первый вариант.
+
+**Плохо**:
+```
+POST /v1/coffee-machines/search
+{
+ "recipes": ["lngo"]
+ // Положение пользователя не задано
+}
+→
+{
+ "results": [
+ // Результаты для какой-то
+ // локации по умолчанию
+ ]
+}
+```
+
+Результатом подобных политик «тихого» исправления ошибок становятся абсурдные ситуации типа «null island» — [самой посещаемой точки в мире](https://www.sciencealert.com/welcome-to-null-island-the-most-visited-place-that-doesn-t-exist). Чем популярнее API, тем больше шансов, что партнеры просто не обратят внимания на такие пограничные ситуации.
+
+**Хорошо**
+```
+POST /v1/coffee-machines/search
+{
+ "recipes": ["lngo"]
+ // Положение пользователя не задано
+}
+→ 400 Bad Request
+{
+ // описание ошибки
+ // см. следующее правило
+}
+```
+
+Желательно не только обращать внимание партнёров на ошибки, но и проактивно предупреждать их о поведении, возможно похожем на ошибку:
+
+```
+POST /v1/coffee-machines/search
+{
+ "recipes": ["lngo"]
+ "position": {
+ "latitude": 0,
+ "longitude": 0
+ },
+ "force_convact_delivery": true
+}
+→
+{
+ "results": [],
+ "warnings": [{
+ "type": "suspicious_coordinates",
+ "message": "Position [0, 0]⮠
+ is probably a mistake"
+ }, {
+ "type": "unknown_field",
+ "message": "unknown field:⮠
+ `force_convact_delivery`. Did you⮠
+ mean `force_contact_delivery`?"
+ }]
+}
+```
+
+Однако следует отметить, что далеко не во все интерфейсы можно удобно уложить дополнительно возврат предупреждений. В такой ситуации можно ввести дополнительный режим отладки или строгий режим, в котором уровень предупреждений эскалируется:
+
+```
+POST /v1/coffee-machines/search⮠
+ strict_mode=true
+{
+ "recipes": ["lngo"]
+ "position": {
+ "latitude": 0,
+ "longitude": 0
+ }
+}
+→ 404 Bad Request
+{
+ "errors": [{
+ "type": "suspicious_coordinates",
+ "message": "Position [0, 0]⮠
+ is probably a mistake"
+ }],
+ …
+}
+```
+
+Если всё-таки координаты [0, 0] не ошибка, то можно дополнительно задавать игнорируемые ошибки для конкретной операции:
+
+```
+POST /v1/coffee-machines/search⮠
+ strict_mode=true⮠
+ disable_errors=suspicious_coordinates
+```
+
+##### Ошибки должны быть информативными
+
+Недостаточно просто валидировать ввод — необходимо ещё и уметь правильно описать, в чём состоит проблема. В ходе работы над интеграцией партнёры неизбежно будут допускать детские ошибки. Чем понятнее тексты сообщений, возвращаемых вашим API, тем меньше времени разработчик потратит на отладку, и тем приятнее работать с таким API.
+
+**Плохо**:
+```
+POST /v1/coffee-machines/search
+{
+ "recipes": ["lngo"],
+ "position": {
+ "latitude": 110,
+ "longitude": 55
+ }
+}
+→ 400 Bad Request
+{}
+```
+— да, конечно, допущенные ошибки (опечатка в `"lngo"` и неправильные координаты) очевидны. Но раз наш сервер всё равно их проверяет, почему не вернуть описание ошибок в читаемом виде?
+
+**Хорошо**:
+```
+{
+ "reason": "wrong_parameter_value",
+ "localized_message":
+ "Что-то пошло не так.⮠
+ Обратитесь к разработчику приложения."
+ "details": {
+ "checks_failed": [
+ {
+ "field": "recipe",
+ "error_type": "wrong_value",
+ "message":
+ "Value 'lngo' unknown.⮠
+ Did you mean 'lungo'?"
+ },
+ {
+ "field": "position.latitude",
+ "error_type": "constraint_violation",
+ "constraints": {
+ "min": -90,
+ "max": 90
+ },
+ "message":
+ "'position.latitude' value⮠
+ must fall within⮠
+ the [-90, 90] interval"
+ }
+ ]
+ }
+}
+```
+Также хорошей практикой является указание всех допущенных ошибок, а не только первой найденной.
+
+##### Декларируйте технические ограничения явно
+
+У любого поля в вашем API есть ограничения на допустимые значения: максимальная длина текста, объём прикладываемых документов в мегабайтах, разрешённые диапазоны цифровых значений. Часто разработчики API пренебрегают указанием этих лимитов — либо потому, что считают их очевидными, либо потому, что попросту не знают их сами. Это, разумеется, один большой антипаттерн: незнание пределов использования системы автоматически означает, что код партнёров может в любой момент перестать работать по не зависящим от них причинам.
+
+Поэтому, во-первых, указывайте границы допустимых значений для всех без исключения полей в API, и, во-вторых, если эти границы нарушены, генерируйте машиночитаемую ошибку с описанием, какое ограничение на какое поле было нарушено.
+
+То же соображение применимо и к квотам: партнёры должны иметь доступ к информации о том, какую долю доступных ресурсов они выбрали, и ошибки в случае превышения квоты должны быть информативными.
+
+##### Указывайте время жизни ресурсов и политики кэширования
+
+В современных системах клиент, как правило, обладает собственным состоянием и почти всегда кэширует результаты запросов — неважно, долговременно ли или в течение сессии: у каждого объекта всегда есть какое-то время автономной жизни. Поэтому желательно вносить ясность; каким образом рекомендуется кэшировать результат должно быть понятно, если не из сигнатур операций, то хотя бы из документации.
+
+Следует уточнить, что кэш мы понимаем в расширенном смысле, а именно: какое варьирование параметров операции (не только времени обращения, но и прочих переменных) следует считать достаточно близким к предыдущему запросу, чтобы можно было использовать результат из кэша?
+
+**Плохо**:
+```
+// Возвращает цену лунго в кафе,
+// ближайшем к указанной точке
+GET /v1/price?recipe=lungo⮠
+ &longitude={longitude}⮠
+ &latitude={latitude}
+→
+{ "currency_code", "price" }
+```
+Возникает два вопроса:
+ * в течение какого времени эта цена действительна?
+ * на каком расстоянии от указанной точки цена всё ещё действительна?
+
+**Хорошо**:
+Для указания времени жизни кэша можно пользоваться стандартными средствами протокола, например, заголовком `Cache-Control`. В ситуации, когда кэш существует не только во временном измерении (как, например, в нашем примере добавляется пространственное измерение), вам придётся разработать свой формат описания параметров кэширования.
+
+```
+// Возвращает предложение: за какую сумму
+// наш сервис готов приготовить лунго
+GET /v1/price?recipe=lungo⮠
+ &longitude={longitude}⮠
+ &latitude={latitude}
+→
+{
+ "offer": {
+ "id",
+ "currency_code",
+ "price",
+ "conditions": {
+ // До какого времени
+ // валидно предложение
+ "valid_until",
+ // Где валидно предложение:
+ // * город
+ // * географический объект
+ // * …
+ "valid_within"
+ }
+ }
+}
+```
+
+##### Сохраняйте точность дробных чисел
+
+Там, где это позволено протоколом, дробные числа с фиксированной запятой — такие, как денежные суммы, например — должны передаваться в виде специально предназначенных для этого объектов, например, Decimal или аналогичных.
+
+Если в протоколе нет Decimal-типов (в частности, в JSON нет чисел с фиксированной запятой), следует либо привести к целому (путём домножения на указанный множитель), либо использовать строковый тип.
+
+Если конвертация в формат с плавающей запятой заведомо приводит к потере точности (например, если мы переведём 20 минут в часы в виде десятичной дроби), то следует либо предпочесть формат без потери точности (т.е. предпочесть формат `00:20` формату `0.333333…`), либо предоставить SDK работы с такими данными, либо (в крайнем случае) описать в документации принципы округления.
+
+##### Все операции должны быть идемпотентны
+
+Напомним, идемпотентность — это следующее свойство: повторный вызов той же операции с теми же параметрами не изменяет результат. Поскольку мы обсуждаем в первую очередь клиент-серверное взаимодействие, узким местом в котором является ненадежность сетевой составляющей, повтор запроса при обрыве соединения — не исключительная ситуация, а норма жизни.
+
+Там, где идемпотентность не может быть обеспечена естественным образом, необходимо добавить явный параметр — ключ идемпотентности или ревизию.
+
+**Плохо**:
+```
+// Создаёт заказ
+POST /orders
+```
+Повтор запроса создаст два заказа!
+
+**Хорошо**:
+```
+// Создаёт заказ
+POST /v1/orders
+X-Idempotency-Token: <случайная строка>
+```
+Клиент на своей стороне запоминает `X-Idempotency-Token`, и, в случае автоматического повторного перезапроса, обязан его сохранить. Сервер на своей стороне проверяет токен и, если заказ с таким токеном уже существует для этого клиента, не даёт создать заказ повторно.
+
+**Альтернатива**:
+```
+// Создаёт черновик заказа
+POST /v1/orders/drafts
+→
+{ "draft_id" }
+```
+```
+// Подтверждает черновик заказа
+PUT /v1/orders/drafts⮠
+ /{draft_id}/confirmation
+{ "confirmed": true }
+```
+Создание черновика заказа — необязывающая операция, которая не приводит ни к каким последствиям, поэтому допустимо создавать черновики без токена идемпотентности.
+Операция подтверждения заказа — уже естественным образом идемпотентна, для неё `draft_id` играет роль ключа идемпотентности.
+
+Также стоит упомянуть, что добавление токенов идемпотентности к эндпойнтам, которые и так изначально идемпотентны, имеет определённый смысл, так как токен помогает различить две ситуации:
+ * клиент не получил ответ из-за сетевых проблем и пытается повторить запрос;
+ * клиент ошибся, пытаясь применить конфликтующие изменения.
+
+Рассмотрим следующий пример: представим, что у нас есть ресурс с общим доступом, контролируемым посредством номера ревизии, и клиент пытается его обновить.
+```
+POST /resource/updates
+{
+ "resource_revision": 123
+ "updates"
+}
+```
+
+Сервер извлекает актуальный номер ревизии и обнаруживает, что он равен 124. Как ответить правильно? Можно просто вернуть `409 Conflict`, но тогда клиент будет вынужден попытаться выяснить причину конфликта и как-то решить его, потенциально запутав пользователя. К тому же, фрагментировать алгоритмы разрешения конфликтов, разрешая каждому клиенту реализовать какой-то свой — плохая идея.
+
+Сервер мог бы попытаться сравнить значения поля `updates`, предполагая, что одинаковые значения означают перезапрос, но это предположение будет опасно неверным (например, если ресурс представляет собой счётчик, то последовательные запросы с идентичным телом нормальны).
+
+Добавление токена идемпотентности (явного в виде случайной строки или неявного в виде черновиков) решает эту проблему
+```
+POST /resource/updates
+X-Idempotency-Token: <токен>
+{
+ "resource_revision": 123
+ "updates"
+}
+→ 201 Created
+```
+— сервер обнаружил, что ревизия 123 была создана с тем же токеном идемпотентности, а значит клиент просто повторяет запрос.
+
+Или:
+```
+POST /resource/updates
+X-Idempotency-Token: <токен>
+{
+ "resource_revision": 123
+ "updates"
+}
+→ 409 Conflict
+```
+— сервер обнаружил, что ревизия 123 была создана с другим токеном, значит имеет место быть конфликт общего доступа к ресурсу.
+
+Более того, добавление токена идемпотентности не только решает эту проблему, но и позволяет в будущем сделать продвинутые оптимизации. Если сервер обнаруживает конфликт общего доступа, он может попытаться решить его, «перебазировав» обновление, как это делают современные системы контроля версий, и вернуть `200 OK` вместо `409 Conflict`. Эта логика существенно улучшает пользовательский опыт и при этом полностью обратно совместима и предотвращает фрагментацию кода разрешения конфликтов.
+
+Но имейте в виду: клиенты часто ошибаются при имплементации логики токенов идемпотентности. Две проблемы проявляются постоянно:
+ * нельзя полагаться на то, что клиенты генерируют честные случайные токены — они могут иметь одинаковый seed рандомизатора или просто использовать слабый алгоритм или источник энтропии; при проверке токенов нужны слабые ограничения: уникальность токена должна проверяться не глобально, а только применительно к конкретному пользователю и конкретной операции;
+ * клиенты склонны неправильно понимать концепцию — или генерировать новый токен на каждый перезапрос (что на самом деле неопасно, в худшем случае деградирует UX), или, напротив, использовать один токен для разнородных запросов (а вот это опасно и может привести к катастрофически последствиям; ещё одна причина имплементировать совет из предыдущего пункта!); поэтому рекомендуется написать хорошую документацию и/или клиентскую библиотеку для перезапросов.
+
+##### Не изобретайте безопасность
+
+Если бы автору этой книги давали доллар каждый раз, когда ему приходилось бы имплементировать кем-то придуманный дополнительный протокол безопасности — он бы давно уже был на заслуженной пенсии. Любовь разработчиков API к подписыванию параметры запросов или сложным схемам обмена паролей на токены столь же несомненна, сколько и бессмысленна.
+
+**Во-первых**, почти всегда процедуры, обеспечивающие безопасность той или иной операции, *уже разработаны*. Нет никакой нужды придумывать их заново, просто имплементируйте какой-то из существующих протоколов. Никакие самописные алгоритмы проверки сигнатур запросов не обеспечат вам того же уровня защиты от атаки [Man-in-the-Middle](https://en.wikipedia.org/wiki/Man-in-the-middle_attack), как соединение по протоколу TLS с взаимной проверкой сигнатур сертификатов.
+
+**Во-вторых**, чрезвычайно самонадеянно (и опасно) считать, что вы разбираетесь в вопросах безопасности. Новые вектора атаки появляются каждый день, и быть в курсе всех актуальных проблем — это само по себе работа на полный рабочий день. Если же вы полный рабочий день занимаетесь чем-то другим, спроектированная вами система защиты наверняка будет содержать уязвимости, о которых вы просто никогда не слышали — например, ваш алгоритм проверки паролей может быть подвержен [атаке по времени](https://en.wikipedia.org/wiki/Timing_attack), а веб-сервер — [атаке с разделением запросов](https://capec.mitre.org/data/definitions/105.html).
+
+Отдельно уточним: любые API должны предоставляться строго по протоколу TLS версии не ниже 1.2 (лучше 1.3).
+
+##### Помогайте партнёрам не изобретать безопасность
+
+Не менее важно не только обеспечивать безопасность API как такового, но и предоставить партнёрам такие интерфейсы, которые минимизируют возможные проблемы с безопасностью на их стороне.
+
+**Плохо**:
+```
+// Позволяет партнёру задать
+// описание для своего напитка
+PUT /v1/partner-api/{partner-id}⮠
+ /recipes/lungo/info
+""
+```
+```
+// возвращает описание
+GET /v1/partner-api/{partner-id}⮠
+ /recipes/lungo/info
+→
+""
+```
+
+Подобный интерфейс является прямым способом соорудить хранимую XSS, которым потенциально может воспользоваться злоумышленник. Да, это ответственность самого партнёра — не допускать сохранения подобного ввода. Но большие цифры по-прежнему работают против вас: всегда найдутся начинающие разработчики, которые не знают об этом виде уязвимости или не подумали о нём. В худшем случае существование таких хранимых уязвимостей может затронуть не только конкретного партнёра, но и вообще всех пользователей API.
+
+В таких ситуациях мы рекомендуем, во-первых, всегда валидировать вводимые через API данные, и, во-вторых, ограничивать радиус взрыва так, чтобы через уязвимости в коде одного партнёра нельзя было затронуть других партнёров. В случае, если функциональность небезопасного ввода всё же нужна, необходимо предупреждать о рисках максимально явно.
+
+**Луче** (но не идеально):
+```
+// Позволяет партнёру задать
+// потенциально небезопасное
+// описание для своего напитка
+PUT /v1/partner-api/{partner-id}⮠
+ /recipes/lungo/info
+X-Dangerously-Disable-Sanitizing: true
+""
+```
+```
+// возвращает потенциально
+// небезопасное описание
+GET /v1/partner-api/{partner-id}⮠
+ /recipes/lungo/info
+X-Dangerously-Allow-Raw-Value: true
+→
+""
+```
+
+В частности, если вы позволяете посредством API выполнять какие-то текстовые скрипты, всегда предпочитайте безопасный ввод небезопасному.
+
+**Плохо**
+```
+POST /v1/run/sql
+{
+ // Передаёт готовый запрос целиком
+ "query": "INSERT INTO data (name)⮠
+ VALUES ('Robert');⮠
+ DROP TABLE students;--')"
+}
+```
+**Лучше**
+```
+POST /v1/run/sql
+{
+ // Передаёт шаблон запроса
+ "query": "INSERT INTO data (name)⮠
+ VALUES (?)",
+ // и параметры для подстановки
+ values: [
+ "Robert');⮠
+ DROP TABLE students;--"
+ ]
+}
+```
+
+Во втором случае вы сможете централизованно экранировать небезопасный ввод и избежать тем самым SQL-инъекции. Напомним повторно, что делать это необходимо с помощью state-of-the-art инструментов, а не самописных регулярных выражений.
+
+##### Используйте глобально уникальные идентификаторы
+
+Хорошим тоном при разработке API будет использование для идентификаторов сущностей глобально уникальных строк, либо семантичных (например, "lungo" для видов напитков), либо случайных (например [UUID-4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random))). Это может чрезвычайно пригодиться, если вдруг придётся объединять данные из нескольких источников под одним идентификатором.
+
+Мы вообще склонны порекомендовать использование идентификаторов в urn-подобном формате, т.е. `urn:order:` (или просто `order:`), это сильно помогает с отладкой legacy-систем, где по историческим причинам есть несколько разных идентификаторов для одной и той же сущности, в таком случае неймспейсы в urn помогут быстро понять, что это за идентификатор и нет ли здесь ошибки использования.
+
+Отдельное важное следствие: **не используйте инкрементальные номера как идентификаторы**. Помимо вышесказанного, это плохо ещё и тем, что ваши конкуренты легко смогут подсчитать, сколько у вас в системе каких сущностей и тем самым вычислить, например, точное количество заказов за каждый день наблюдений.
+
+**NB**: в этой книге часто используются короткие идентификаторы типа "123" в примерах — это для удобства чтения на маленьких экранах, повторять эту практику в реальном API не надо.
+
+##### Предусмотрите ограничения доступа
+
+С ростом популярности API вам неизбежно придётся внедрять технические средства защиты от недобросовестного использования — такие, как показ капчи, расстановка приманок-honeypot-ов, возврат ошибок вида «слишком много запросов», постановка прокси-защиты от DDoS перед эндпойнтами и так далее. Всё это невозможно сделать, если вы не предусмотрели такой возможности изначально, а именно — не ввели соответствующей номенклатуры ошибок и предупреждений.
+
+Вы не обязаны с самого начала такие ошибки действительно генерировать — но вы можете предусмотреть их на будущее. Например, вы можете описать ошибку `429 Too Many Requests` или перенаправление на показ капчи, но не имплементировать возврат таких ответов, пока не возникнет в этом необходимость.
+
+Отдельно необходимо уточнить, что в тех случаях, когда через API можно совершать платежи, ввод дополнительных факторов аутентификации пользователя (через TOTP, SMS или технологии типа 3D-Secure) должен быть предусмотрен обязательно.
+
+##### Не предоставляйте endpoint-ов массового получения чувствительных данных
+
+Если через API возможно получение персональных данных, номер банковских карт, переписки пользователей и прочей информации, раскрытие которой нанесёт большой ущерб пользователям, партнёрам и/или вам — методов массового получения таких данных в API быть не должно, или, по крайней мере, на них должны быть ограничения на частоту запросов, размер страницы данных, а в идеале ещё и многофакторная аутентификация.
+
+Часто разумной практикой является предоставление таких массовых выгрузок по запросу, т.е. фактически в обход API.
+
+##### Локализация и интернационализация
+
+Все эндпойнты должны принимать на вход языковые параметры (например, в виде заголовка `Accept-Language`), даже если на текущем этапе нужды в локализации нет.
+
+Важно понимать, что язык пользователя и юрисдикция, в которой пользователь находится — разные вещи. Цикл работы вашего API всегда должен хранить локацию пользователя. Либо она задаётся явно (в запросе указываются географические координаты), либо неявно (первый запрос с географическими координатами инициировал создание сессии, в которой сохранена локация) — но без локации корректная локализация невозможна. В большинстве случаев локацию допустимо редуцировать до кода страны.
+
+Дело в том, что множество параметров, потенциально влияющих на работу API, зависят не от языка, а именно от расположения пользователя. В частности, правила форматирования чисел (разделители целой и дробной частей, разделители разрядов) и дат, первый день недели, раскладка клавиатуры, система единиц измерения (которая к тому же может оказаться не десятичной!) и так далее. В некоторых ситуациях необходимо хранить две локации: та, в которой пользователь находится, и та, которую пользователь сейчас просматривает. Например, если пользователь из США планирует туристическую поездку в Европу, то цены ему желательно показывать в местной валюте, но отформатированными согласно правилам американского письма.
+
+Следует иметь в виду, что явной передачи локации может оказаться недостаточно, поскольку в мире существуют территориальные конфликты и спорные территории. Каким образом API должен себя вести при попадании координат пользователя на такие территории — вопрос, к сожалению, в первую очередь юридический. Автору этой книги приходилось как-то разрабатывать API, в котором пришлось вводить концепцию «территория государства A по мнению официальных органов государства Б».
+
+**Важно**: различайте локализацию для конечного пользователя и локализацию для разработчика. В примере из п. 12 сообщение `localized_message` адресовано пользователю — его должно показать приложение, если в коде обработка такой ошибки не предусмотрена. Это сообщение должно быть написано на указанном в запросе языке и отформатировано согласно правилам локации пользователя. А вот сообщение `details.checks_failed[].message` написано не для пользователя, а для разработчика, который будет разбираться с проблемой. Соответственно, написано и отформатировано оно должно быть понятным для разработчика образом — что, скорее всего, означает «на английском языке», т.к. английский де-факто является стандартом в мире разработки программного обеспечения.
+
+Следует отметить, что индикация, какие сообщения следует показать пользователю, а какие написаны для разработчика, должна, разумеется, быть явной конвенцией вашего API. В примере для этого используется префикс `localized_`.
+
+И ещё одна вещь: все строки должны быть в кодировке UTF-8 и никакой другой.
diff --git a/src/ru/drafts/03-Раздел II. Паттерны API/Observability.md b/src/ru/drafts/03-Раздел II. Паттерны API/Observability.md
new file mode 100644
index 0000000..d62fd0f
--- /dev/null
+++ b/src/ru/drafts/03-Раздел II. Паттерны API/Observability.md
@@ -0,0 +1,83 @@
+#### Правила разработки машиночитаемых интерфейсов
+
+В погоне за понятностью API для людей мы часто забываем, что работать с API всё-таки будут не сами разработчики, а написанный ими код. Многие концепции, которые хорошо работают для визуальных интерфейсов, плохо подходят для интерфейсов программных: в частности, разработчик не может в коде принимать решения, ориентируясь на текстовые сообщения, и не может «выйти и зайти снова» в случае нештатной ситуации.
+
+##### Состояние системы должно быть понятно клиенту
+
+Часто можно встретить интерфейсы, в которых клиент не обладает полнотой знаний о том, что происходит в системе от его имени — например, какие операции сейчас выполняются и каков их статус.
+
+**Плохо**:
+```
+// Создаёт заказ и возвращает его id
+POST /v1/orders
+{ … }
+→
+{ "order_id" }
+```
+```
+// Возвращает заказ по его id
+GET /v1/orders/{id}
+// Заказ ещё не подтверждён
+// и ожидает проверки
+→ 404 Not Found
+```
+— хотя операция будто бы выполнена успешно, клиенту необходимо самостоятельно запомнить идентификатор заказа и периодически проверять состояние `GET /v1/orders/{id}`. Этот паттерн плох сам по себе, но ещё и усугубляется двумя обстоятельствами:
+
+ * клиент может потерять идентификатор, если произошёл системный сбой в момент между отправкой запроса и получением ответа или было повреждено (очищено) системное хранилище данных приложения;
+ * потребитель не может воспользоваться другим устройством; фактически, знание о сделанном заказе привязано к конкретному юзер-агенту.
+
+В обоих случаях потребитель может решить, что заказ по какой-то причине не создался — и сделать повторный заказ со всеми вытекающими отсюда проблемами.
+
+**Хорошо**:
+```
+// Создаёт заказ и возвращает его
+POST /v1/orders
+{ <параметры заказа> }
+→
+{
+ "order_id",
+ // Заказ создаётся в явном статусе
+ // «идёт проверка»
+ "status": "checking",
+ …
+}
+```
+```
+// Возвращает заказ по его id
+GET /v1/orders/{id}
+→
+{ "order_id", "status" … }
+```
+```
+// Возвращает все заказы пользователя
+// во всех статусах
+GET /v1/users/{id}/orders
+```
+
+Это правило также распространяется и на ошибки, в первую очередь, клиентские. Если ошибку можно исправить, информация об этом должна быть машиночитаема.
+
+**Плохо**: `{ "error": "email malformed" }`
+— единственное, что может с этой ошибкой сделать разработчик — показать её пользователю
+
+**Хорошо**:
+```
+{
+ // Машиночитаемый статус
+ "status": "validation_failed",
+ // Массив описания проблем;
+ // если пользовательский ввод
+ // некорректен в нескольких
+ // аспектах, пользователь сможет
+ // исправить их все
+ "failed_checks": [
+ {
+ "field: "email",
+ "error_type": "malformed",
+ // Локализованное
+ // человекочитаемое
+ // сообщение
+ "message": "email malformed"
+ }
+ ]
+}
+```
diff --git a/src/ru/drafts/03-Раздел II. Паттерны API/Атомарность.md b/src/ru/drafts/03-Раздел II. Паттерны API/Атомарность.md
new file mode 100644
index 0000000..6ee442c
--- /dev/null
+++ b/src/ru/drafts/03-Раздел II. Паттерны API/Атомарность.md
@@ -0,0 +1,150 @@
+##### Избегайте неатомарных операций
+
+С применением массива изменений часто возникает вопрос: что делать, если часть изменений удалось применить, а часть — нет? Здесь правило очень простое: если вы можете обеспечить атомарность, т.е. выполнить либо все изменения сразу, либо ни одно из них — сделайте это.
+
+**Плохо**:
+```
+// Возвращает список рецептов
+api.getRecipes();
+→
+{
+ "recipes": [{
+ "id": "lungo",
+ "volume": "200ml"
+ }, {
+ "id": "latte",
+ "volume": "300ml"
+ }]
+}
+
+// Изменяет параметры
+api.updateRecipes({
+ "changes": [{
+ "id": "lungo",
+ "volume": "300ml"
+ }, {
+ "id": "latte",
+ "volume": "-1ml"
+ }]
+});
+→
+Bad Request
+
+// Перечитываем список
+api.getRecipes();
+→
+{
+ "recipes": [{
+ "id": "lungo",
+ // Это значение изменилось
+ "volume": "300ml"
+ }, {
+ "id": "latte",
+ // А это нет
+ "volume": "300ml"
+ }]
+}
+```
+— клиент никак не может узнать, что операция, которую он посчитал ошибочной, на самом деле частично применена. Даже если индицировать это в ответе, у клиента нет способа понять — значение объёма лунго изменилось вследствие запроса, или это конкурирующее изменение, выполненное другим клиентом.
+
+Если способа обеспечить атомарность выполнения операции нет, следует очень хорошо подумать над её обработкой. Следует предоставить способ получения статуса каждого изменения отдельно.
+
+**Лучше**:
+```
+api.updateRecipes({
+ "changes": [{
+ "recipe_id": "lungo",
+ "volume": "300ml"
+ }, {
+ "recipe_id": "latte",
+ "volume": "-1ml"
+ }]
+});
+// Можно воспользоваться статусом
+// «частичного успеха»,
+// если он предусмотрен протоколом
+→
+{
+ "changes": [{
+ "change_id",
+ "occurred_at",
+ "recipe_id": "lungo",
+ "status": "success"
+ }, {
+ "change_id",
+ "occurred_at",
+ "recipe_id": "latte",
+ "status": "fail",
+ "error"
+ }]
+}
+```
+
+Здесь:
+ * `change_id` — уникальный идентификатор каждого атомарного изменения;
+ * `occurred_at` — время проведения каждого изменения;
+ * `error` — информация по ошибке для каждого изменения, если она возникла.
+
+Не лишним будет также:
+ * ввести в запросе `sequence_id`, чтобы гарантировать порядок исполнения операций и соотнесение порядка статусов изменений в ответе с запросом;
+ * предоставить отдельный эндпойнт `/changes-history`, чтобы клиент мог получить информацию о выполненных изменениях, если во время обработки запроса произошла сетевая ошибка или приложение перезагрузилось.
+
+Неатомарные изменения нежелательны ещё и потому, что вносят неопределённость в понятие идемпотентности, даже если каждое вложенное изменение идемпотентно. Рассмотрим такой пример:
+
+```
+api.updateRecipes({
+ "idempotency_token",
+ "changes": [{
+ "recipe_id": "lungo",
+ "volume": "300ml"
+ }, {
+ "recipe_id": "latte",
+ "volume": "400ml"
+ }]
+});
+→
+{
+ "changes": [{
+ …
+ "status": "success"
+ }, {
+ …
+ "status": "fail",
+ "error": {
+ "reason":
+ "too_many_requests"
+ }
+ }]
+}
+```
+
+Допустим, клиент не смог получить ответ и повторил запрос с тем же токеном идемпотентности.
+
+```
+api.updateRecipes({
+ "idempotency_token",
+ "changes": [{
+ "recipe_id": "lungo",
+ "volume": "300ml"
+ }, {
+ "recipe_id": "latte",
+ "volume": "400ml"
+ }]
+});
+→
+{
+ "changes": [{
+ …
+ "status": "success"
+ }, {
+ …
+ "status": "success",
+ }]
+}
+```
+
+По сути, для клиента всё произошло ожидаемым образом: изменения были внесены, и последний полученный ответ всегда корректен. Однако по сути состояние ресурса после первого запроса отличалось от состояния ресурса после второго запроса, что противоречит самому определению идемпотентности.
+
+Более корректно было бы при получении повторного запроса с тем же токеном ничего не делать и возвращать ту же разбивку ошибок, что была дана на первый запрос — но для этого придётся её каким-то образом хранить в истории изменений.
+
+На всякий случай уточним, что вложенные операции должны быть сами по себе идемпотентны. Если же это не так, то следует сгенерировать внутренние ключи идемпотентности на каждую вложенную операцию в отдельности.
diff --git a/src/ru/drafts/03-Раздел II. Паттерны API/Ошибки.md b/src/ru/drafts/03-Раздел II. Паттерны API/Ошибки.md
new file mode 100644
index 0000000..b70ba3e
--- /dev/null
+++ b/src/ru/drafts/03-Раздел II. Паттерны API/Ошибки.md
@@ -0,0 +1,105 @@
+##### Соблюдайте правильный порядок ошибок
+
+**Во-первых**, всегда показывайте неразрешимые ошибки прежде разрешимых:
+```
+POST /v1/orders
+{
+ "recipe": "lngo",
+ "offer"
+}
+→ 409 Conflict
+{
+ "reason": "offer_expired"
+}
+// Повторный запрос
+// с новым `offer`
+POST /v1/orders
+{
+ "recipe": "lngo",
+ "offer"
+}
+→ 400 Bad Request
+{
+ "reason": "recipe_unknown"
+}
+```
+— какой был смысл получать новый `offer`, если заказ всё равно не может быть создан?
+
+**Во-вторых**, соблюдайте такой порядок разрешимых ошибок, который приводит к наименьшему раздражению пользователя и разработчика. В частности, следует начинать с более значимых ошибок, решение которых требует более глобальных изменений.
+
+**Плохо**:
+```
+POST /v1/orders
+{
+ "items": [{
+ "item_id": "123",
+ "price": "0.10"
+ }]
+}
+→
+409 Conflict
+{
+ "reason": "price_changed",
+ "details": [{
+ "item_id": "123",
+ "actual_price": "0.20"
+ }]
+}
+// Повторный запрос
+// с актуальной ценой
+POST /v1/orders
+{
+ "items": [{
+ "item_id": "123",
+ "price": "0.20"
+ }]
+}
+→
+409 Conflict
+{
+ "reason": "order_limit_exceeded",
+ "localized_message":
+ "Лимит заказов превышен"
+}
+```
+— какой был смысл показывать пользователю диалог об изменившейся цене, если и с правильной ценой заказ он сделать всё равно не сможет? Пока один из его предыдущих заказов завершится и можно будет сделать следующий заказ, цену, наличие и другие параметры заказа всё равно придётся корректировать ещё раз.
+
+**В-третьих**, постройте схему: разрешение какой ошибки может привести к появлению другой, иначе вы можете показать одну и ту же ошибку несколько раз, а то и вовсе зациклить разрешение ошибок.
+
+```
+// Создаём заказ с платной доставкой
+POST /v1/orders
+{
+ "items": 3,
+ "item_price": "3000.00"
+ "currency_code": "MNT",
+ "delivery_fee": "1000.00",
+ "total": "10000.00"
+}
+→ 409 Conflict
+// Ошибка: доставка становится бесплатной
+// при стоимости заказа от 9000 тугриков
+{
+ "reason": "delivery_is_free"
+}
+
+// Создаём заказ с бесплатной доставкой
+POST /v1/orders
+{
+ "items": 3,
+ "item_price": "3000.00"
+ "currency_code": "MNT",
+ "delivery_fee": "0.00",
+ "total": "9000.00"
+}
+→ 409 Conflict
+// Ошибка: минимальная сумма заказа
+// 10000 тугриков
+{
+ "reason": "below_minimal_sum",
+ "currency_code": "MNT",
+ "minimal_sum": "10000.00"
+}
+```
+
+Легко заметить, что в этом примере нет способа разрешить ошибку в один шаг — эту ситуацию требуется предусмотреть отдельно, и либо изменить параметры расчёта (минимальная сумма заказа не учитывает скидки), либо ввести специальную ошибку для такого кейса.
diff --git a/src/ru/drafts/03-Раздел II. Паттерны API/Пагинация.md b/src/ru/drafts/03-Раздел II. Паттерны API/Пагинация.md
new file mode 100644
index 0000000..fe5b69c
--- /dev/null
+++ b/src/ru/drafts/03-Раздел II. Паттерны API/Пагинация.md
@@ -0,0 +1,134 @@
+##### Пагинация, фильтрация и курсоры
+
+Любой эндпойнт, возвращающий массивы данных, должен содержать пагинацию. Никаких исключений в этом правиле быть не может.
+
+Любой эндпойнт, возвращающий изменяемые данные постранично, должен обеспечивать возможность эти данные перебрать.
+
+**Плохо**:
+```
+// Возвращает указанный limit записей,
+// отсортированных по дате создания
+// начиная с записи с номером offset
+GET /v1/records?limit=10&offset=100
+```
+На первый взгляд это самый что ни на есть стандартный способ организации пагинации в API. Однако зададим себе три вопроса.
+ 1. Каким образом клиент узнает о появлении новых записей в начале списка?
+ Легко заметить, что клиент может только попытаться повторить первый запрос и сверить идентификаторы с запомненным началом списка. Но что делать, если добавленное количество записей превышает `limit`? Представим себе ситуацию:
+ * клиент обрабатывает записи в порядке поступления;
+ * произошла какая-то проблема, и накопилось большое количество необработанных записей;
+ * клиент запрашивает новые записи (`offset=0`), однако не находит на первой странице известных идентификаторов — новых записей накопилось больше, чем `limit`;
+ * клиент вынужден продолжить перебирать записи (увеличивая `offset`) до тех пор, пока не доберётся до последней известной ему; всё это время клиент простаивает;
+ * таким образом может сложиться ситуация, когда клиент вообще никогда не обработает всю очередь, т.к. будет занят беспорядочным линейным перебором.
+ 2. Что произойдёт, если при переборе списка одна из записей в уже перебранной части будет удалена?
+ Произойдёт следующее: клиент пропустит одну запись и никогда не сможет об этом узнать.
+ 3. Какие параметры кэширования мы можем выставить на этот эндпойнт?
+ Никакие: повторяя запрос с теми же `limit`-`offset`, мы каждый раз получаем новый набор записей.
+
+**Хорошо**: в таких однонаправленных списках пагинация должна быть организована по тому ключу, порядок сортировки по которому фиксирован. Например, вот так:
+```
+// Возвращает указанный limit записей,
+// отсортированных по дате создания,
+// начиная с первой записи,
+// созданной позднее,
+// чем запись с указанным id
+GET /v1/records⮠
+ ?older_than={record_id}&limit=10
+// Возвращает указанный limit записей,
+// отсортированных по дате создания,
+// начиная с первой записи,
+// созданной раньше,
+// чем запись с указанным id
+GET /v1/records⮠
+ ?newer_than={record_id}&limit=10
+```
+
+При такой организации клиенту не надо заботиться об удалении или добавлении записей в уже перебранной части списка: он продолжает перебор по идентификатору известной записи — первой известной, если надо получить новые записи; последней известной, если надо продолжить перебор.
+Если операции удаления записей нет, то такие запросы можно свободно кэшировать — по одному и тому же URL будет всегда возвращаться один и тот же набор записей.
+Другой вариант организации таких списков — возврат курсора `cursor`, который используется вместо `record_id`, что делает интерфейсы более универсальными.
+
+```
+// Первый запрос данных
+POST /v1/records/list
+{
+ // Какие-то дополнительные
+ // параметры фильтрации
+ "filter": {
+ "category": "some_category",
+ "created_date": {
+ "older_than": "2020-12-07"
+ }
+ }
+}
+→
+{ "cursor" }
+```
+
+```
+// Последующие запросы
+GET /v1/records?cursor=<курсор>
+{ "records", "cursor" }
+```
+
+Достоинством схемы с курсором является возможность зашифровать в самом курсоре данные исходного запроса (т.е. `filter` в нашем примере), и таким образом не дублировать его в последующих запросах. Это может быть особенно актуально, если инициализирующий запрос готовит полный массив данных, например, перенося его из «холодного» хранилища в горячее.
+
+Вообще схему с курсором можно реализовать множеством способов (например, не разделять первый и последующие запросы данных), главное — выбрать какой-то один.
+
+**NB**: в некоторых источниках такой подход, напротив, не рекомендуется по следующей причине: пользователю невозможно показать список страниц и дать возможность выбрать произвольную. Здесь следует отметить, что:
+ * подобный кейс — список страниц и выбор страниц — существует только для пользовательских интерфейсов; представить себе API, в котором действительно требуется доступ к случайным страницам данных мы можем с очень большим трудом;
+ * если же мы всё-таки говорим об API приложения, которое содержит элемент управления с постраничной навигацией, то наиболее правильный подход — подготавливать данные для этого элемента управления на стороне сервера, в т.ч. генерировать ссылки на страницы;
+ * подход с курсором не означает, что `limit`/`offset` использовать нельзя — ничто не мешает сделать двойной интерфейс, который будет отвечать и на запросы вида `GET /items?cursor=…`, и на запросы вида `GET /items?offset=…&limit=…`;
+ * наконец, если возникает необходимость предоставлять доступ к произвольной странице в пользовательском интерфейсе, то следует задать себе вопрос, какая проблема тем самым решается; вероятнее всего с помощью этой функциональности пользователь что-то ищет: определенный элемент списка или может быть позицию, на которой он закончил работу со списком в прошлый раз; возможно, для этих задач следует предоставить более удобные элементы управления, нежели перебор страниц.
+
+**Плохо**:
+```
+// Возвращает указанный limit записей,
+// отсортированных по полю sort_by
+// в порядке sort_order,
+// начиная с записи с номером offset
+GET /records?sort_by=date_modified⮠
+ &sort_order=desc&limit=10&offset=100
+```
+
+Сортировка по дате модификации обычно означает, что данные могут меняться. Иными словами, между запросом первой порции данных и запросом второй порции данных какая-то запись может измениться; она просто пропадёт из перечисления, т.к. автоматически попадает на первую страницу. Клиент никогда не получит те записи, которые менялись во время перебора, и у него даже нет способа узнать о самом факте такого пропуска. Помимо этого отметим, что такой API нерасширяем — невозможно добавить сортировку по двум и более полям.
+
+**Хорошо**: в представленной постановке задача, собственно говоря, не решается. Список записей по дате изменения всегда будет непредсказуемо изменяться, поэтому необходимо изменить сам подход к формированию данных, одним из двух способов.
+
+**Вариант 1**: фиксировать порядок в момент обработки запроса; т.е. сервер формирует полный список и сохраняет его в неизменяемом виде:
+
+```
+// Создаёт представление по указанным параметрам
+POST /v1/record-views
+{
+ sort_by: [{
+ "field": "date_modified",
+ "order": "desc"
+ }]
+}
+→
+{ "id", "cursor" }
+```
+
+```
+// Позволяет получить часть представления
+GET /v1/record-views/{id}⮠
+ ?cursor={cursor}
+```
+
+Поскольку созданное представление уже неизменяемо, доступ к нему можно организовать как угодно: через курсор, limit/offset, заголовок Range и т.д. Однако надо иметь в виду, что при переборе таких списков порядок может быть нарушен: записи, изменённые уже после генерации представления, будут находиться не на своих местах (либо быть неактуальны, если запись копируется целиком).
+
+**Вариант 2**: гарантировать строгий неизменяемый порядок записей, например, путём введения понятия события изменения записи:
+
+```
+// Курсор опционален
+GET /v1/records/modified/list⮠
+ ?[cursor={cursor}]
+→
+{
+ "modified": [
+ { "date", "record_id" }
+ ],
+ "cursor"
+}
+```
+
+Недостатком этой схемы является необходимость заводить отдельное индексированное хранилище событий, а также появление множества событий для одной записи, если данные меняются часто.
diff --git a/src/ru/drafts/03-Раздел II. Паттерны API/Совместный доступ.md b/src/ru/drafts/03-Раздел II. Паттерны API/Совместный доступ.md
new file mode 100644
index 0000000..1a3d93a
--- /dev/null
+++ b/src/ru/drafts/03-Раздел II. Паттерны API/Совместный доступ.md
@@ -0,0 +1,137 @@
+##### Избегайте неявных частичных обновлений
+
+Один из самых частых антипаттернов в разработке API — попытка сэкономить на подробном описании изменения состояния.
+
+**Плохо**:
+
+```
+// Создаёт заказ из двух напитков
+POST /v1/orders/
+{
+ "delivery_address",
+ "items": [{
+ "recipe": "lungo",
+ }, {
+ "recipe": "latte",
+ "milk_type": "oats"
+ }]
+}
+→
+{ "order_id" }
+```
+
+```
+// Частично перезаписывает заказ
+// обновляет объём второго напитка
+PATCH /v1/orders/{id}
+{
+ "items": [null, {
+ "volume": "800ml"
+ }]
+}
+→
+{ /* изменения приняты */ }
+```
+
+Эта сигнатура плоха сама по себе, поскольку является нечитабельной. Что обозначает пустой первый элемент массива — это удаление элемента или указание на отсутствие изменений? Что произойдёт с полями, которые не указаны в операции обновления (`delivery_address`, `milk_type`) — они будут сброшены в значения по умолчанию или останутся неизменными?
+
+Самое неприятное здесь — какой бы вариант вы ни выбрали, это только начало проблем. Допустим, мы договорились, что конструкция `{"items":[null, {…}]}` означает, что с первым элементом массива ничего не происходит, он не меняется. А как тогда всё-таки его удалить? Придумать ещё одно «зануляемое» значение специально для удаления? Аналогично, если значения неуказанных полей остаются без изменений — как сбросить их в значения по умолчанию?
+
+**Простое решение** состоит в том, чтобы всегда перезаписывать объект целиком, т.е. требовать передачи полного объекта, полностью заменять им текущее состояние и возвращать в ответ на операцию новое состояние целиком. Однако это простое решение часто не принимается по нескольким причинам:
+ * повышенные размеры запросов и, как следствие, расход трафика;
+ * необходимость вычислять, какие конкретно поля изменились — в частности для того, чтобы правильно сгенерировать сигналы (события) для подписчиков на изменения;
+ * невозможность совместного доступа к объекту, когда два клиента независимо редактируют его свойства.
+
+Все эти соображения, однако, на поверку оказываются мнимыми:
+ * причины увеличенного расхода трафика мы разбирали выше, и передача лишних полей к ним не относится (а если и относится, то это повод декомпозировать эндпойнт);
+ * концепция передачи только изменившихся полей по факту перекладывает ответственность определения, какие поля изменились, на клиент;
+ * это не только не снижает сложность имплементации этого кода, но и чревато его фрагментацией на несколько независимых клиентских реализаций;
+ * существование клиентского алгоритма построения diff-ов не отменяет обязанность сервера уметь делать то же самое — поскольку клиентские разработчики могли ошибиться или просто полениться правильно вычислить изменившиеся поля;
+ * наконец, подобная наивная концепция организации совместного доступа работает ровно до того момента, пока изменения транзитивны, т.е. результат не зависит от порядка выполнения операций (в нашим примере это уже не так — операции удаления первого элемента и редактирования первого элемента нетранзитивны);
+ * кроме того, часто в рамках той же концепции экономят и на входящем трафике, возвращая пустой ответ сервера для модифицирующих операций; таким образом, два клиента, редактирующих одну и ту же сущность, не видят изменения друг друга.
+
+**Лучше**: разделить эндпойнт. Этот подход также хорошо согласуется [с принципом декомпозиции](#chapter-10), который мы рассматривали в предыдущем разделе.
+
+```
+// Создаёт заказ из двух напитков
+POST /v1/orders/
+{
+ "parameters": {
+ "delivery_address"
+ }
+ "items": [{
+ "recipe": "lungo",
+ }, {
+ "recipe": "latte",
+ "milk_type": "oats"
+ }]
+}
+→
+{
+ "order_id",
+ "created_at",
+ "parameters": {
+ "delivery_address"
+ }
+ "items": [
+ { "item_id", "status"},
+ { "item_id", "status"}
+ ]
+}
+```
+
+```
+// Изменяет параметры,
+// относящиеся ко всему заказу
+PUT /v1/orders/{id}/parameters
+{ "delivery_address" }
+→
+{ "delivery_address" }
+```
+
+```
+// Частично перезаписывает заказ
+// обновляет объём одного напитка
+PUT /v1/orders/{id}/items/{item_id}
+{
+ // Все поля передаются, даже если
+ // изменилось только какое-то одно
+ "recipe", "volume", "milk_type"
+}
+→
+{ "recipe", "volume", "milk_type" }
+```
+
+```
+// Удаляет один из напитков в заказе
+DELETE /v1/orders/{id}/items/{item_id}
+```
+
+Теперь для удаления `volume` достаточно *не* передавать его в `PUT items/{item_id}`. Кроме того, обратите внимание, что операции удаления одного напитка и модификации другого теперь стали транзитивными.
+
+Этот подход также позволяет отделить неизменяемые и вычисляемые поля (`created_at` и `status`) от изменяемых, не создавая двусмысленных ситуаций (что произойдёт, если клиент попытается изменить `created_at`?).
+
+Также в ответах операций `PUT` можно возвращать объект заказа целиком, а не перезаписываемый суб-ресурс (однако следует использовать какую-то конвенцию именования).
+
+**NB**: при декомпозиции эндпойнтов велик соблазн провести границу так, чтобы разделить изменяемые и неизменяемые данные. Тогда последние можно объявить кэшируемыми условно вечно и вообще не думать над проблемами пагинации и формата обновления. На бумаге план выглядит отлично, однако с ростом API неизменяемые данные частенько перестают быть таковыми, и вся концепция не только перестаёт работать, но и выглядит как плохой дизайн. Мы скорее рекомендуем объявлять данные иммутабельными в одном из двух случаев: либо (1) они действительно не могут стать изменяемыми без слома обратной совместимости, либо (2) ссылка на ресурс (например, на изображение) поступает через API же, и вы обладаете возможностью сделать эти ссылки персистентными (т.е. при необходимости обновить изображение будете генерировать новую ссылку, а не перезаписывать контент по старой ссылке).
+
+**Ещё лучше**: разработать формат описания атомарных изменений.
+
+```
+POST /v1/order/changes
+X-Idempotency-Token: <токен идемпотентности>
+{
+ "changes": [{
+ "type": "set",
+ "field": "delivery_address",
+ "value": <новое значение>
+ }, {
+ "type": "unset_item_field",
+ "item_id",
+ "field": "volume"
+ }],
+ …
+}
+```
+
+Этот подход существенно сложнее в имплементации, но является единственным возможным вариантом реализации совместного редактирования, поскольку он явно отражает, что в действительности делал пользовать с представлением объекта. Имея данные в таком формате возможно организовать и оффлайн-редактирование, когда пользовательские изменения накапливаются и сервер впоследствии автоматически разрешает конфликты, «перебазируя» изменения.
diff --git a/src/ru/drafts/03-Раздел II. Паттерны API/Трафик.md b/src/ru/drafts/03-Раздел II. Паттерны API/Трафик.md
new file mode 100644
index 0000000..a96d81a
--- /dev/null
+++ b/src/ru/drafts/03-Раздел II. Паттерны API/Трафик.md
@@ -0,0 +1,23 @@
+##### Считайте трафик
+
+В современном мире такой ресурс, как объём пропущенного трафика, считать уже почти не принято — считается, что Интернет всюду практически безлимитен. Однако он всё-таки не абсолютно безлимитен: всегда можно спроектировать систему так, что объём трафика окажется некомфортным даже и для современных сетей.
+
+Три основные причины раздувания объёма трафика достаточно очевидны:
+ * не предусмотрен постраничный перебор данных;
+ * не предусмотрены ограничения на размер значений полей и/или передаются большие бинарные данные (графика, аудио, видео и т.д.);
+ * клиент слишком часто запрашивает данные и/или слишком мало их кэширует.
+
+Если первые две проблемы решаются чисто техническими средствами (см. соответствующие разделы), то третья проблема скорее логическая: каким образом разумно организовать канал обновления состояния клиента так, чтобы найти баланс между отзывчивостью системы и затраченными на эту отзывчивость ресурсами. Здесь мы можем дать несколько рекомендаций:
+
+ * не злоупотребляйте асинхронными интерфейсами;
+ * с одной стороны, они позволяют нивелировать многие технические проблемы с производительностью API, что, в свою очередь, позволяет поддерживать обратную совместимость: если метод изначально асинхронный, то можно без проблем увеличивать время обработки и менять модель консистентности данных;
+ * с другой стороны, количество генерируемых клиентами запросов становится трудно предсказуемым, поскольку для получения результата клиенту необходимо сделать заранее неизвестное число обращений;
+
+ * объявляйте явную политику перезапросов (например, посредством заголовка `Retry-After`);
+ * да, какие-то клиенты будут её игнорировать, т.к. разработчики поленятся её имплементировать, но какие-то не будут (особенно если вы сами предоставляете SDK);
+
+ * если вы ожидаете значительного количества асинхронных операций в API, изначально дайте разработчику выбор между моделями poll (клиент самостоятельно производит новые запросы к API чтобы проверить, не изменился ли статус асинхронной операций) и push (сервер уведомляет клиентов об изменениях статусов посредством отправки специального запроса, например, через webhook-и или server push-механизмы);
+
+ * если в рамках одной сущности необходимо предоставлять как «лёгкие» (скажем, название и описание рецепта), так и «тяжёлые» данные (скажем, промо-фотография напитка, которая легко может по размеру превышать текстовые поля в сотни раз), лучше разделить эндпойнты и отдавать только ссылку для доступа к «тяжёлым» данным (в нашем случае, ссылку на изображение) — это, как минимум, позволит задавать различные политики кэширования для разных данных.
+
+Неплохим упражнением здесь будет промоделировать типовой жизненный цикл основной функциональности приложения партнёра (например, выполнение одного заказа) и подсчитать общее количество запросов и объём трафика на один цикл.
\ No newline at end of file
diff --git a/src/ru/drafts/03-Раздел II. Паттерны API/Хелперы, обратная совместимость, работа с умолчаниями.md b/src/ru/drafts/03-Раздел II. Паттерны API/Хелперы, обратная совместимость, работа с умолчаниями.md
new file mode 100644
index 0000000..95f3458
--- /dev/null
+++ b/src/ru/drafts/03-Раздел II. Паттерны API/Хелперы, обратная совместимость, работа с умолчаниями.md
@@ -0,0 +1,84 @@
+##### Избегайте неявного приведения типов
+
+Этот совет парадоксально противоположен предыдущему. Часто при разработке API возникает ситуация, когда добавляется новое необязательное поле с непустым значением по умолчанию. Например:
+
+```
+const orderParams = {
+ contactless_delivery: false
+};
+const order = api.createOrder(
+ orderParams
+);
+```
+
+Новая опция `contactless_delivery` является необязательной, однако её значение по умолчанию — `true`. Возникает вопрос, каким образом разработчик должен отличить явное *нежелание* пользоваться опцией (`false`) от незнания о её существовании (поле не задано). Приходится писать что-то типа такого:
+
+```
+if (
+ Type(
+ orderParams.contactless_delivery
+ ) == 'Boolean' &&
+ orderParams
+ .contactless_delivery == false) {
+ …
+}
+```
+
+Эта практика ведёт к усложнению кода, который пишут разработчики, и в этом коде легко допустить ошибку, которая по сути меняет значение поля на противоположное. То же самое произойдёт, если для индикации отсутствия значения поля использовать специальное значение типа `null` или `-1`.
+
+**NB**. Это замечание не распространяется на те случаи, когда платформа и протокол однозначно и без всяких дополнительных абстракций поддерживают такие специальные значения для сброса значения поля в значение по умолчанию. Однако полная и консистентная поддержка частичных операций со сбросом значений полей практически нигде не имплементирована. Пожалуй, единственный пример такого API из имеющих широкое распространение сегодня — SQL: в языке есть и концепция `NULL`, и значения полей по умолчанию, и поддержка операций вида `UPDATE … SET field = DEFAULT` (в большинстве диалектов). Хотя работа с таким протоколом всё ещё затруднена (например, во многих диалектах нет простого способа получить обратно значение по умолчанию, которое выставил `UPDATE … DEFAULT`), логика работы с умолчаниями в SQL имплементирована достаточно хорошо, чтобы использовать её как есть.
+
+Если же протоколом явная работа со значениями по умолчанию не предусмотрена, универсальное правило — все новые необязательные булевы флаги должны иметь значение по умолчанию false.
+
+**Хорошо**
+```
+const orderParams = {
+ force_contact_delivery: true
+};
+const order = api.createOrder(
+ orderParams
+);
+```
+
+Если же требуется ввести небулево поле, отсутствие которого трактуется специальным образом, то следует ввести пару полей.
+
+**Плохо**:
+```
+// Создаёт пользователя
+POST /v1/users
+{ … }
+→
+// Пользователи создаются по умолчанию
+// с указанием лимита трат в месяц
+{
+ "spending_monthly_limit_usd": "100",
+ …
+}
+// Для отмены лимита требуется
+// указать значение null
+PUT /v1/users/{id}
+{
+ "spending_monthly_limit_usd": null,
+ …
+}
+```
+
+**Хорошо**
+```
+POST /v1/users
+{
+ // true — у пользователя снят
+ // лимит трат в месяц
+ // false — лимит не снят
+ // (значение по умолчанию)
+ "abolish_spending_limit": false,
+ // Необязательное поле, имеет смысл
+ // только если предыдущий флаг
+ // имеет значение false
+ "spending_monthly_limit_usd": "100",
+ …
+}
+```
+
+**NB**: противоречие с предыдущим советом состоит в том, что мы специально ввели отрицающий флаг («нет лимита»), который по правилу двойных отрицаний пришлось переименовать в `abolish_spending_limit`. Хотя это и хорошее название для отрицательного флага, семантика его довольно неочевидна, разработчикам придётся как минимум покопаться в документации. Таков путь.
+
diff --git a/src/ru/drafts/План.md b/src/ru/drafts/План.md
index 96c33aa..b5ccc4d 100644
--- a/src/ru/drafts/План.md
+++ b/src/ru/drafts/План.md
@@ -1,43 +1,43 @@
-1. Зачем предоставлять API
- * возможности интеграции с основным сервисом
- * непрофильные сервисы
- * концепция API-first
-2. Выгоды наличия API и виды монетизации
- * API к монетизируемым сервисам (+ Affiliate API)
- * on-premise
- * freemium-модели
- * лимиты
- * ограничения на типы использования
- * техническая поддержка
- * премиум-функциональность
- * маркетплейсы
- * рекламные модели
- * сбор данных / UGC
- * контакт с брендом
- * терраформирование
-3. Прямая и обратная API-пирамида Маслоу
-4. Виды API
- * API
- * SDK
- * виджеты
- * embedded (включая картинки)
-5. Бизнес-аудитория, её интересы и места обитания
- * SLA
- * безопасность
- * идентификация пользователей, ведение статистики, защита публичных ключей
- * контакты и оповещения
-6. Разработчики, их интересы и места обитания
- * новички vs продвинутые
- * OpenSource
-7. Документация
- * референс
- * tutorial
- * how-to
- * примеры
- * песочница
-8. Поддержка пользователей
- * форумы
- * поддержка разработчиков
- * работа с комьюнити
- * обратная связь
-9. Продуктовое управление разработкой API
\ No newline at end of file
+Раздел I
+ * описание конечных советов — оставить только кодстайл
+Раздел II
+ * API-first подход
+ * выбор поддерживаемых стандартов
+ * существующие стандарты (описания) взаимодействия через API
+ * JSON / OpenAPI
+ * XML-RPC / WSDL
+ * JSON-RPC
+ * GraphQL
+ * GRPC
+ * клиентские библиотеки
+ * хелперы, обратная совместимость, работа с умолчаниями
+ * синхронное и асинхронное взаимодействие
+ * push- и poll-модели
+ * сильная и слабая консистентность
+ * машиночитаемое API
+ * observability
+ * перебор списков, курсоры
+ * объёмы передаваемых данных
+ * сжатие
+ * кэширование
+ * ошибки
+ * разрешимость
+ * сведение к умолчанию
+ * ошибка для пользователя vs ошибка для разработчика
+ * деградация
+ * мониторинг состояния
+Раздел III
+ * о терминологии
+ * введение в HTTP
+ * REST, определение и реальность
+ * плюсы и минусы разработки HTTP API
+ * проблема статус-кодов
+ * трактовка методов
+ * CRUD
+ * кодстайл
+ * domain/path/query/body
+Раздел IV
+ * постановка проблемы
+ * реюз кода vs реюз поведения
+ * трехстороннее взаимодействие
+ * асинхронность, разделяемые ресурсы
\ No newline at end of file