You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-07-12 22:50:21 +02:00
Chapter 7 rewritten finished
This commit is contained in:
BIN
docs/API.ru.epub
BIN
docs/API.ru.epub
Binary file not shown.
944
docs/API.ru.html
944
docs/API.ru.html
File diff suppressed because it is too large
Load Diff
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -525,6 +525,52 @@ GET /v1/users/{id}/orders
|
||||
}
|
||||
```
|
||||
|
||||
##### Указывайте время жизни ресурсов и политики кэширования
|
||||
|
||||
В современных системах клиент, как правило, обладает собственным состоянием и почти всегда кэширует результаты запросов — неважно, долговременно ли или в течение сессии: у каждого объекта всегда есть какое-то время автономной жизни. Желательно в такой ситуации вносить ясность; каким образом рекомендуется кэшировать результат должно быть понятно, если не из сигнатур операций, то хотя бы из документации.
|
||||
|
||||
Следует уточнить, что кэш мы понимаем в расширенном смысле, а именно: какое варьирование параметров операции (не только времени обращения, но и прочих переменных) следует считать повтором запроса?
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
// Возвращает цену лунго в кафе,
|
||||
// ближайшем к указанной точке
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Пагинация, фильтрация и курсоры
|
||||
|
||||
Любой эндпойнт, возвращающий массивы данных, должен содержать пагинацию. Никаких исключений в этом правиле быть не может.
|
||||
@ -927,115 +973,102 @@ PATCH /v1/recipes
|
||||
|
||||
Неплохим упражнением здесь будет промоделировать типовой жизненный цикл основной функциональности приложения (например, выполнение одного заказа) и подсчитать общее количество запросов и объём трафика на один цикл.
|
||||
|
||||
##### Указывайте политики кэширования
|
||||
|
||||
В клиент-серверном 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Избегайте неявных частичных обновлений
|
||||
|
||||
Один из самых частых антипаттернов в разработке API — попытка сэкономить на подробном описании изменения состояния.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
// Возвращает состояние заказа
|
||||
// по его идентификатору
|
||||
GET /v1/orders/123
|
||||
→
|
||||
// Создаёт заказ из двух напитков
|
||||
POST /v1/orders/
|
||||
{
|
||||
"order_id",
|
||||
"delivery_address",
|
||||
"client_phone_number",
|
||||
"client_phone_number_ext",
|
||||
"updated_at"
|
||||
"items": [{
|
||||
"recipe": "lungo",
|
||||
}, {
|
||||
"recipe": "latte",
|
||||
"milk_type": "oats"
|
||||
}]
|
||||
}
|
||||
// Частично перезаписывает заказ
|
||||
PATCH /v1/orders/123
|
||||
{ "delivery_address" }
|
||||
→
|
||||
{ "delivery_address" }
|
||||
{ "order_id" }
|
||||
```
|
||||
```
|
||||
// Частично перезаписывает заказ
|
||||
// обновляет объём второго напитка
|
||||
PATCH /v1/orders/{id}
|
||||
{"items": [null, {
|
||||
"volume": "800ml"
|
||||
}]}
|
||||
→
|
||||
{ /* изменения приняты */ }
|
||||
```
|
||||
— такой подход часто практикуют для того, чтобы уменьшить объёмы запросов и ответов, плюс это позволяет дёшево реализовать совместное редактирование. Оба этих преимущества на самом деле являются мнимыми.
|
||||
|
||||
**Во-первых**, экономия объёма ответа в таком формате бессмысленна. Максимальные размеры сетевых пакетов (MTU, Maximum Transmission Unit) в настоящее время составляют более килобайта; пытаться экономить на размере ответа, пока он не превышает килобайт — попросту бессмысленная трата времени. Реальные причины слишком высокого расхода трафика мы описали выше.
|
||||
Эта сигнатура плоха сама по себе, поскольку является нечитабельной. Что обозначает пустой первый элемент массива — это удаление элемента или указание на отсутствие изменений? Что произойдёт с полями, которые не указаны в операции обновления (`delivery_address`, `milk_type`) — они будут сброшены в значения по умолчанию или останутся неизменными? Ну и самое неприятное — какой бы вариант вы ни выбрали, это только начало проблем.
|
||||
|
||||
**Во-вторых**, экономия размера ответа выйдет боком как раз при совместном редактировании: один клиент не будет видеть, какие изменения внёс другой. Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа не оказывает значительного влияния на производительность) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.
|
||||
Допустим, мы договорились, что конструкция `{"items":[null, {…}]}` означает, что с первым элементом массива ничего не происходит, он не меняется. А как тогда всё-таки его удалить? Придумать ещё одно «зануляемое» значение специально для удаления? Аналогично, если значения неуказанных полей остаются без изменений — как сбросить их в значения по умолчанию?
|
||||
|
||||
**В-третьих**, этот подход может как-то работать при необходимости перезаписать поле. Но что делать, если поле требуется сбросить к значению по умолчанию? Например, как *удалить* `client_phone_number_ext`?
|
||||
**Простое решение** состоит в том, чтобы всегда перезаписывать объект целиком, т.е. требовать передачи полного объекта и полностью заменять им текущее состояние и возвращать в ответ на операцию новое состояние целиком. Однако это простое решение часто не принимается по нескольким причинам:
|
||||
* повышенные размеры запросов и, как следствие, расход трафика;
|
||||
* необходимость вычислять, какие конкретно поля изменились — в частности для того, чтобы правильно сгенерировать сигналы (события) для подписчиков на изменения;
|
||||
* невозможность совместного доступа к объекту, когда два клиента независимо редактируют его свойства.
|
||||
|
||||
Часто в таких случаях прибегают к специальным значениям, которые означают удаление поля, например, `null`. Но, как мы разобрали выше, это плохая практика. Другой вариант — запрет необязательных полей, но это существенно усложняет дальнейшее развитие API.
|
||||
Все эти соображения, однако, на поверку оказываются мнимыми:
|
||||
* причины увеличенного расхода трафика мы разбирали выше, и передача лишних полей к ним не относится (а если и относится, то это повод декомпозировать эндпойнт);
|
||||
* концепция передачи только изменившихся полей по факту перекладывает ответственность определения, какие поля изменились, на клиент — что не только не снижает сложность имплементации этого кода, но и чревато его фрагментацией на несколько независимых клиентских реализаций; более того, существование клиентского алгоритма построения diff-ов не отменяет обязанность сервера уметь делать то же самое — поскольку клиент может ошибиться или просто полениться правильно вычислить изменившиеся поля;
|
||||
* наконец, подобная наивная концепция организации совместного доступа работает ровно до того момента, пока изменения транзитивны, т.е. результат не зависит от порядка выполнения операций (в нашим примере это уже не так — операции удаления первого элемента и редактирования первого элемента нетранзитивны);
|
||||
* кроме того, часто в рамках той же экономии ответ на операцию частичного обновления пуст; таким образом, два клиента, редактирующих одну и ту же сущность, не видят изменения друг друга.
|
||||
|
||||
**Хорошо**: можно применить одну из двух стратегий.
|
||||
|
||||
**Вариант 1**: разделение эндпойнтов. Редактируемые поля группируются и выносятся в отдельный эндпойнт. Этот подход также хорошо согласуется [с принципом декомпозиции](#chapter-10), который мы рассматривали в предыдущем разделе.
|
||||
**Лучше**: разделить эндпойнты. Редактируемые поля группируются и выносятся в отдельный эндпойнт. Этот подход также хорошо согласуется [с принципом декомпозиции](#chapter-10), который мы рассматривали в предыдущем разделе.
|
||||
|
||||
```
|
||||
// Возвращает состояние заказа
|
||||
// по его идентификатору
|
||||
GET /v1/orders/123
|
||||
```
|
||||
// Создаёт заказ из двух напитков
|
||||
POST /v1/orders/
|
||||
{
|
||||
"parameters": {
|
||||
"delivery_address"
|
||||
}
|
||||
"items": [{
|
||||
"recipe": "lungo",
|
||||
}, {
|
||||
"recipe": "latte",
|
||||
"milk_type": "oats"
|
||||
}]
|
||||
}
|
||||
→
|
||||
{
|
||||
"order_id",
|
||||
"delivery_details": {
|
||||
"address"
|
||||
},
|
||||
"client_details": {
|
||||
"phone_number",
|
||||
"phone_number_ext"
|
||||
},
|
||||
"updated_at"
|
||||
"created_at",
|
||||
"parameters": {
|
||||
"delivery_address"
|
||||
}
|
||||
// Полностью перезаписывает
|
||||
// информацию о доставке заказа
|
||||
PUT /v1/orders/123/delivery-details
|
||||
{ "address" }
|
||||
// Полностью перезаписывает
|
||||
// информацию о клиенте
|
||||
PUT /v1/orders/123/client-details
|
||||
{ "phone_number" }
|
||||
"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"}
|
||||
```
|
||||
```
|
||||
|
||||
Теперь для удаления `client_phone_number_ext` достаточно *не* передавать его в `PUT client-details`. Этот подход также позволяет отделить неизменяемые и вычисляемые поля (`order_id` и `updated_at`) от изменяемых, не создавая двусмысленных ситуаций (что произойдёт, если клиент попытается изменить `updated_at`?). В этом подходе также можно в ответах операций `PUT` возвращать объект заказа целиком (однако следует использовать какую-то конвенцию именования).
|
||||
Теперь для удаления `volume` достаточно *не* передавать его в `PUT items/{item_id}`. Этот подход также позволяет отделить неизменяемые и вычисляемые поля (`created_at` и `status`) от изменяемых, не создавая двусмысленных ситуаций (что произойдёт, если клиент попытается изменить `created_at`?). В этом подходе также можно в ответах операций `PUT` возвращать объект заказа целиком (однако следует использовать какую-то конвенцию именования), а не только изменённые суб-объекты.
|
||||
|
||||
**Вариант 2**: разработать формат описания атомарных изменений.
|
||||
**Ещё лучше**: разработать формат описания атомарных изменений.
|
||||
|
||||
```
|
||||
POST /v1/order/changes
|
||||
@ -1046,9 +1079,11 @@ X-Idempotency-Token: <см. следующий раздел>
|
||||
"field": "delivery_address",
|
||||
"value": <новое значение>
|
||||
}, {
|
||||
"type": "unset",
|
||||
"field": "client_phone_number_ext"
|
||||
}]
|
||||
"type": "unset_item_field",
|
||||
"item_id",
|
||||
"field": "volume"
|
||||
}],
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
@ -1068,11 +1103,11 @@ X-Idempotency-Token: <см. следующий раздел>
|
||||
|
||||
**NB**: в этой книге часто используются короткие идентификаторы типа "123" в примерах — это для удобства чтения на маленьких экранах, повторять эту практику в реальном API не надо.
|
||||
|
||||
##### Предусмотрите ограничения
|
||||
##### Предусмотрите ограничения доступа
|
||||
|
||||
С ростом популярности API вам неизбежно придётся внедрять технические средства защиты от недобросовестного использования — такие, как показ капчи, расстановка приманок-honeypot-ов, возврат ошибок вида «слишком много запросов», постановка прокси-защиты от DDoS перед эндпойнтами и так далее. Всё это невозможно сделать, если вы не предусмотрели такой возможности изначально, а именно — не ввели соответствующей номенклатуры ошибок и предупреждений.
|
||||
|
||||
Вы не обязаны с самого начала такие ошибки действительно генерировать — но вы можете предусмотреть их на будущее. Например, вы можете описать ошибку `429 Too Many Requests` или перенаправление на показ капчи, но не имплементировать возврат таких ответов, пока не возникнет такая необходимость.
|
||||
Вы не обязаны с самого начала такие ошибки действительно генерировать — но вы можете предусмотреть их на будущее. Например, вы можете описать ошибку `429 Too Many Requests` или перенаправление на показ капчи, но не имплементировать возврат таких ответов, пока не возникнет в этом необходимость.
|
||||
|
||||
Отдельно необходимо уточнить, что в тех случаях, когда через API можно совершать платежи, ввод дополнительных факторов аутентификации пользователя (через TOTP, SMS или технологии типа 3D-Secure) должен быть предусмотрен обязательно.
|
||||
|
||||
|
Reference in New Issue
Block a user