From e2761c00554ff6952deeb2700167a03a05fd8497 Mon Sep 17 00:00:00 2001 From: Sergey Konstantinov Date: Tue, 20 Jun 2023 00:14:44 +0300 Subject: [PATCH] Designing URLs begin --- .../03.md | 2 + .../04.md | 2 + .../06.md | 30 +++- .../07.md | 2 +- .../03.md | 2 + .../04.md | 2 + .../06.md | 154 +++++++++++++++++- .../07.md | 2 +- 8 files changed, 192 insertions(+), 4 deletions(-) diff --git a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/03.md b/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/03.md index 81e535f..bf830d1 100644 --- a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/03.md +++ b/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/03.md @@ -92,6 +92,8 @@ HTTP verbs define two important characteristics of an HTTP call: | POST | Processes a provided entity according to its internal semantics | No | No | Yes | | PATCH | Modifies (partially overwrites) a resource with a provided entity | No | No | Yes | +**NB**: contrary to a popular misconception, the `POST` method is not limited to creating new resources. + The most important property of modifying idempotent verbs is that **the URL serves as an idempotency key for the request**. The `PUT /url` operation fully overwrites a resource, so repeating the request won't change the resource. Conversely, retrying a `DELETE /url` request must leave the system in the same state where the `/url` resource is deleted. Regarding the `GET /url` method, it must semantically return the representation of the same target resource `/url`. If it exists, its implementation must be consistent with prior `PUT` / `DELETE` operations. If the resource was overwritten via `PUT /url`, a subsequent `GET /url` call must return a representation that matches the entity enclosed in the `PUT /url` request. In the case of JSON-over-HTTP APIs, this simply means that `GET /url` returns the same data as what was passed in the preceding `PUT /url`, possibly normalized and equipped with default values. On the other hand, a `DELETE /url` call must remove the resource, resulting in subsequent `GET /url` requests returning a `404` or `410` error. The idempotency and symmetry of the `GET` / `PUT` / `DELETE` methods imply that neither `GET` nor `DELETE` can have a body as no reasonable meaning could be associated with it. However, most web server software allows these methods to have bodies and transmits them further to the endpoint handler, likely because many software engineers are unaware of the semantics of the verbs (although we strongly discourage relying on this behavior). diff --git a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/04.md b/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/04.md index 38bbd10..56b8e3e 100644 --- a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/04.md +++ b/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/04.md @@ -44,6 +44,8 @@ Additionally, let's emphasize that the HTTP API paradigm is currently the defaul The main disadvantage of HTTP APIs is that you have to rely on intermediary agents, from client frameworks to API gateways, to read the request metadata and perform actions based on it *without your consent*. This includes regulating timeouts and retry policies, logging, proxying, and sharding requests, among other things. Since HTTP-related specifications are complex and the concepts of REST can be challenging to comprehend, and software engineers do not always write perfect code, these intermediary agents (including partners' developers!) will sometimes interpret HTTP metadata *incorrectly*, especially when dealing with exotic and hard-to-implement standards. Usually, one of the stated reasons for developing new RPC frameworks is the desire to make working with the protocol simple and consistent, thereby reducing the likelihood of errors when writing integration code. +This conclusion applies not only to software but also to its creators. Developers' knowledge of HTTP APIs is fragmented as well. Almost every programmer is capable of working with HTTP APIs to some extent, but a significant number of them lack a thorough understanding of the standards and do not consult them while writing code. As a result, implementing business logic that effectively and consistently works with HTTP APIs can be more challenging than integrating alternative technologies. This observation holds true for both partner integrators and API providers themselves. + #### The Question of Performance When discussing the advantages of alternative technologies such as GraphQL, gRPC, Apache Thrift, etc., the argument of lower performance of JSON-over-HTTP APIs is often presented. Specifically, the following issues with the technology are commonly mentioned: diff --git a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/06.md b/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/06.md index 8cd8fe5..f37e3b8 100644 --- a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/06.md +++ b/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/06.md @@ -1 +1,29 @@ -### Working with HTTP API Errors \ No newline at end of file +### [Designing a Nomenclature of URLs and Applicable Operations][http-api-urls-crud] + +As we noted on several occasions in the previous chapters, neither the HTTP and URL standards nor REST architectural principles prescribe concrete semantics to meaningful parts of a URL (notably, path fragments and key-value pairs in query). **The rules of organizing URLs in an HTTP API exist *only* for improving API's readability and consistency from the developers' perspective**. However, this doesn't mean they are not important. Quite the opposite: URLs in HTTP APIs are meanings of describing abstraction levels and entities' responsibility areas. Fine API hierarchy design must be reflected in a fine URL nomenclature design. + +**NB**: the lack of specific guidance from the specification editors naturally led to inventing it by developers themselves. Many of these spontaneous practices that can be found on the Internet, such as the requirement to use only nouns in URLs, are claimed to be set in the standards or REST architectural principles (which they are not). Nevertheless, deliberately ignoring such self-proclaimed “best practices” isn't the best approach for an API vendor as it increases the chances to be misunderstood. + +Traditionally, the following semantics is considered to be the default one: + * Path components (i.e., fragments between `/` symbols) are used to organize nested resources, such as `/partner/{id}/coffee-machines/{id}`. A path might be extended further by adding new suffixes indicated subordinate sub-resources. + * Query parameters are used to indicate non-strict connections (i.e., “many-to-many” relations such as `/recipes/?partner=`) or as a meaning to pass operation parameters (`/search/?recipe=lungo`). + +This convention allows for decently reflecting almost any API's entity nomenclature and it is more than reasonable to follow it (and it's unreasonable to defiantly neglect it). However, this indistinctly defined logic inevitably leads to numerous variants of reading it: + + 1. How exactly should the endpoints connecting two entities lacking clear relation between them be organized? For example, how should a URL for preparing a lungo on a specific coffee machine look like? + * `/coffee-machines/{id}/recipes/lungo/prepare` + * `/recipes/lungo/coffee-machines/{id}/prepare` + * `/coffee-machines/{id}/prepare?recipe=lungo` + * `/recipes/lungo/prepare?coffee_machine_id=` + * `/prepare?coffee_machine_id=&recipe=lungo` + * `/action=prepare&coffee_machine_id=&recipe=lungo` + + All these options are semantically viable and are generally speaking equitable. + + 2. How strictly should the literal interpretation of the `VERB /resource` construct be enforced? If we agree to follow the “only nouns in the URLs” rule (quite logically, a verb cannot be applied to a verb, right?) then we should use `preparer` or `preparator` in the examples above (and the `/action=prepare&coffee_machine_id=&recipe=lungo` is unacceptable at all as there is no object to act upon). This, however, adds a visual noise in the form of “ator” suffixes but definitely doesn't make code more laconic or readable. + + 3. If the call signature implies that the operation is by default unsafe or non-idempotent, does it mean that the operation *must* be unsafe or non-idempotent? As HTTP verbs bear double semantics (the meaning of the operation vs. possible side effects), it implies the ambiguity in organizing APIs. Let's consider the `/v1/search` resource from from our study API. With which verb should it be requested? + * On one hand, `GET /v1/search?query=` explicitly declares there is no side effects (no state is overwritten) and the results can be cached (given all significant parameters are passed as parts of the URL). + * On the other hand, a response to a `GET /v1/search` request must contain *a representation of the `/search` resource*. Are search results a representation of a search engine? The meaning of a “search” operation is much better described as “processing the representation enclosed in the request according to the resource's own specific semantics,” which is exactly the definition of the `POST` method. Additionally, how could we cache search requests? The results page is formed dynamically from a plethora of various sources, and the subsequent request with the same query might emit a different result. + + In other words, with any operation that runs an algorithm rather than returns a predefined result (such as listing offers relevant to a search phrase), we will have to decide what to choose: following verb semantics or indicating side effects? Caching results or hint the operation is not returning some stable dataset? \ No newline at end of file diff --git a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/07.md b/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/07.md index 49bf19a..8cd8fe5 100644 --- a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/07.md +++ b/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/07.md @@ -1 +1 @@ -### Organizing the HTTP API Resources and Operations \ No newline at end of file +### Working with HTTP API Errors \ No newline at end of file diff --git a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/03.md b/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/03.md index b2dd7e0..ee93415 100644 --- a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/03.md +++ b/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/03.md @@ -100,6 +100,8 @@ HTTP-глагол определяет два важных свойства HTTP | POST | Обрабатывает запрос в соответствии со своим внутренним устройством | нет | нет | да | | PATCH | Модифицирует (частично перезаписывает) ресурс согласно данным, переданным в теле запроса | нет | нет | да | +**NB**: распространено мнение, что метод `POST` предназначен только для создания новых ресурсов. Это совершенно не так, создание ресурса только один из вариантов «обработки запроса согласно внутреннему устройству» эндпойнта. + Важное свойство модифицирующих идемпотентных глаголов — это то, что **URL запроса является его ключом идемпотентности**. `PUT /url` полностью перезаписывает ресурс, заданный своим URL (`/url`), и, таким образом, повтор запроса не изменяет ресурс. Аналогично, повторный вызов `DELETE /url` должен оставить систему в том же состоянии (ресурс `/url` удалён). Учитывая, что метод `GET /url` семантически должен вернуть представление целевого ресурса `/url`, то, если этот метод реализован, он должен возвращать консистентное предыдущим `PUT` / `DELETE` представление. Если ресурс был перезаписан через `PUT /url`, `GET /url` должен вернуть представление, соответствующее переданном в `PUT /url` телу (в случае JSON-over-HTTP API это, как правило, просто означает, что `GET /url` возвращает в точности тот же контент, чтобы передан в `PUT /url`, с точностью до значений полей по умолчанию). `DELETE /url` обязан удалить указанный ресурс — так, что `GET /url` должен вернуть `404` или `410`. Идемпотентность и симметричность методов `GET` / `PUT` / `DELETE` влечёт за собой нежелательность для `GET` и `DELETE` запросов иметь тело (поскольку этому телу невозможно приписать никакой осмысленной роли). Однако (по-видимому в связи с тем, что многие разработчики попросту не знают семантику этих методов) распространённое ПО веб-серверов обычно разрешает этим методам иметь тело запроса и транслирует его дальше к коду обработки эндпойнта (использование этой практики мы решительно не рекомендуем). diff --git a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/04.md b/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/04.md index e009f62..1412b21 100644 --- a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/04.md +++ b/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/04.md @@ -44,6 +44,8 @@ HTTP/1.1 200 OK Главным недостатком HTTP API является то, что промежуточные агенты, от клиентских фреймворков до API-гейтвеев, умеют читать метаданные запроса и выполнять какие-то действия с их использованием — настраивать политику перезапросов и таймауты, логировать, кэшировать, шардировать, проксировать и так далее — даже если вы их об этом не просили. Более того, так как стандарты HTTP являются сложными, концепция REST — непонятной, а разработчики программного обеспечения — неидеальными, то промежуточные агенты (и разработчики партнёра!) могут трактовать метаданные запроса *неправильно*. Особенно это касается каких-то экзотических и сложных в имплементации стандартов. Как правило, одной из причин разработки новых RPC-фреймворков декларируется стремление обеспечить простоту и консистентность работы с протоколом, чтобы таким образом уменьшить поле для потенциальных ошибок в реализации интеграции с API. +Указанное выше соображение распространяется не только на программное обеспечение, но и на его создателей. Представление разработчиков о HTTP API, увы, также фрагментировано. Практически любой программист как-то умеет работать с HTTP API, но редко при этом знает стандарт или хотя бы консультируется с ним при написании кода. Это ведёт к тому, что добиться качественной и консистентной реализации логики работы с HTTP API может быть сложнее, нежели при использовании альтернативных технологий — причём это соображение справедливо как для партнёров-интеграторов, так и для самого провайдера API. + #### Вопросы производительности В пользу многих современных альтернатив HTTP API — таких как GraphQL, gRPC, Apache Thrift — часто приводят аргумент о низкой производительности JSON-over-HTTP API по сравнению с рассматриваемой технологией; конкретнее, называются следующие проблемы: diff --git a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/06.md b/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/06.md index 677d7cb..fadeed1 100644 --- a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/06.md +++ b/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/06.md @@ -1 +1,153 @@ -### Работа с ошибками в HTTP API \ No newline at end of file +### [Разработка номенклатуры URL ресурсов и операций над ними][http-api-urls-crud] + +Как мы уже отмечали в предыдущих главах, стандарты HTTP и URL, а также принципы REST, не предписывают определённой семантики значимым компонентам URL (в частности, частям path и парам ключ-значение в query). **Правила организации URL в HTTP API существуют *только* для читабельности кода и удобства разработчика**. Что, впрочем, совершенно не означает, что они неважны: напротив, URL в HTTP API являются средством выразить уровни абстракции и области ответственности объектов. Правильный дизайн иерархии сущностей в API должен быть отражён в правильном дизайне номенклатуры URL. + +**NB**: отсутствие строгих правил естественным образом привело к тому, что многие разработчики их просто придумали сами для себя. Некоторые наиболее распространённые стихийные практики, например, требование использовать в URL только существительные, в советах по разработке HTTP API в Интернете часто выдаются за стандарты или требования REST, которыми они не являются. Тем не менее, демонстративное игнорирование таких самопровозглашённых правил тоже не лучший подход для провайдера API, поскольку он увеличивает шансы быть неверно понятым. + +Традиционно частям URL приписывается следующая семантика: + * части path (фрагменты пути между символами `/`) используются для организации вложенных сущностей вида `/partner/{id}/coffee-machines/{id}`; при этом путь часто может наращиваться, т.е. к конкретному пути продолжают приписываться новые суффиксы, указывающие на подчинённые ресурсы; + * query используется для организации нестрогой иерархии (отношений «многие ко многим», например `/recipes/?partner=`) либо как способ передать параметры операции (`/search/?recipe=lungo`). + +Подобная конвенция достаточно хорошо подходит для того, чтобы отразить номенклатуру сущностей почти любого API, поэтому следовать ей вполне разумно (и, наоборот, демонстративное нарушение этого устоявшегося соглашения чревато тем, что разработчики вас просто неправильно поймут). Однако подобная некодифицированная и размытая концепция неизбежно вызывает множество разночтений в конкретных моментах: + + 1. Каким образом организовывать эндпойнты, связывающие две сущности, между которыми нет явных отношений подчинения? Скажем, каким должен быть URL запуска приготовления лунго на конкретной кофе-машине? + * `/coffee-machines/{id}/recipes/lungo/prepare` + * `/recipes/lungo/coffee-machines/{id}/prepare` + * `/coffee-machines/{id}/prepare?recipe=lungo` + * `/recipes/lungo/prepare?coffee_machine_id=` + * `/prepare?coffee_machine_id=&recipe=lungo` + * `/?action=prepare&coffee_machine_id=&recipe=lungo` + + Все эти варианты семантически вполне допустимы и в общем-то равноправны. + + 2. Насколько строго должна выдерживаться буквальная интерпретация конструкции `ГЛАГОЛ /ресурс`? Если мы принимаем правило «части URL обязаны быть существительными» (и ведь странно применять глагол к глаголу!), то в примерах выше должно быть не `prepare`, а `preparator` или `preparer` (а вариант `/action=prepare&coffee_machine_id=&recipe=lungo` вовсе недопустим, так как нет объекта действия), что, честно говоря, лишь добавляет визуального шума в виде суффиксов «ator», но никак не способствует большей лаконичности и однозначности понимания. + + 3. Если сигнатура вызова по умолчанию модифицирующая или неидемпотентная, означает ли это, что операция *обязана* быть модифицирующей / идемпотентной? Двойственность смысловой нагрузки глаголов (семантика vs побочные действия) порождает неопределённость в вопросах организации API. Рассмотрим, например, ресурс `/v1/search`, осуществляющий поиск предложений кофе в нашем учебном API. С каким глаголом мы должны к нему обращаться? + * С одной стороны, `GET /v1/search?query=<поисковый запрос>` позволяет явно продекларировать, что никаких посторонних эффектов у этого запроса нет (никакие данные не перезаписываются) и результаты его можно кэшировать (при условии, что все значимые параметры передаются в URL). + * С другой стороны, согласно семантике операции, `GET /v1/search` должен возвращать *представление ресурса `search`*. Но разве результаты поиска являются представлением ресурса-поисковика? Смысл операции «поиск» гораздо точнее описывается фразой «обработка запроса в соответствии с внутренней семантикой ресурса», т.е. соответствует методу `POST`. Кроме того, можем ли мы вообще говорить о кэшировании поисковых запросов? Страница результатов поиска формируется динамически из множества источников, и повторный запрос с той же поисковой фразой почти наверняка выдаст другой список результатов. + + Иными словами, для любых операций, результат которых представляет собой результат работы какого-то алгоритма (например, список релевантных предложений по запросу) мы всегда будем сталкиваться с выбором, что важнее: семантика глагола или отсутствие побочных эффектов? Кэширование ответа или индикация того, что операция вычисляет результаты на лету? + +Простых ответов на вопросы выше у нас, к сожалению, нет (особенно если мы добавим к ним механики логирования и построения мониторингов по URL запроса). В рамках настоящей книги мы придерживаемся следующего подхода: + + * сигнатура вызова в первую очередь должна быть лаконична и читабельна; усложнение сигнатур в угоду абстрактным концепциям нежелательно; + * иерархия ресурсов выдерживается там, где она однозначна (т.е., если сущность низшего уровня абстракции однозначно подчинена сущности высшего уровня абстракции, то отношения между ними будут выражены в виде вложенных путей); + * если есть сомнения в том, что иерархия в ходе дальнейшего развития API останется неизменной, лучше завести новый верхнеуровневый префикс, а не вкладывать новые сущности в уже существующие; + * семантика HTTP-глагола приоритетнее ложного предупреждения о небезопасности/неидемпотентности (в частности, если операция является безопасной, но ресурсозатратной, с нашей точки зрения вполне разумно использовать метод `POST` для индикации этого факта); + * для выполнения кросс-доменных операций предпочтительнее завести специальный ресурс, выполняющий операцию (т.е. в примере с кофе-машинами и рецептами автор этой книги выбрал бы вариант `/prepare?coffee_machine_id=&recipe=lungo`). + +**NB**: отметим, что передача параметров в виде пути или query-параметра в URL влияет не только на читабельность. Если представить, что гейтвей D реализован в виде stateless прокси с декларативной конфигурацией, то получать от клиента запрос в виде: + * `GET /v1/state?user_id=` + + и преобразовывать в пару вложенных запросов + + * `GET /v1/profiles?user_id=` + * `GET /v1/orders?user_id=` + + гораздо удобнее, чем извлекать идентификатор из path и преобразовывать его в query-параметр. Первую операцию [замена одного path целиком на другой] достаточно просто описать декларативно, и в большинстве ПО для веб-серверов она поддерживается из коробки. Напротив, извлечение данных из разных компонентов и полная пересборка запроса — достаточно сложная функциональность, которая, скорее всего, потребует от гейтвея поддержки скриптового языка программирования и/или написания специального модуля для таких манипуляций. Аналогично, автоматическое построение мониторинговых панелей в популярных сервисах типа связки Prometheus+Grafana гораздо проще организовать по path, чем вычленять из данных запроса какой-то синтетический ключ группировки запросов. + + Всё это приводит нас к соображению, что поддержание одинаковой структуры URL, в которой меняется только путь или домен, а параметры всегда находятся в query и именуются одинаково, приводит к ещё более унифицированному интерфейсу, хотя бы и в ущерб читабельности и семантичности URL. Во многих внутренних системах выбор в пользу удобства выглядит самоочевидным, хотя во внешних API мы бы такой подход не рекомендовали. + +**NB**: passing variables as either query parameters or path fragments affects not only readability. If gateway D is implemented as a stateless proxy with a declarative configuration, then receiving a request like: + * `GET /v1/state?user_id=` + + and transforming it into a pair of nested sub-requests: + + * `GET /v1/profiles?user_id=` + * `GET /v1/orders?user_id=` + + would be much more convenient than extracting identifiers from the path or some header and putting them into query parameters. The former operation [replacing one path with another] is easily described declaratively and is supported by most server software out of the box. On the other hand, retrieving data from various components and rebuilding requests is a complex functionality that most likely requires a gateway supporting scripting languages and/or plugins for such manipulations. Conversely, automated creation of monitoring panels in services like Prometheus+Grafana bundle is much easier to organize by path prefix than by a synthetic key computed from request parameters. + + All this leads us to the conclusion than maintaining an identical URL structure when only the path changes while custom parameters are passed in queries will lead to an even more uniform interface, although less readable and semantic. In internal systems, preferring convenience of usage over readability is sometimes an obvious decision. In public APIs, we would rather discourage implementing this approach. + +#### CRUD-операции + +Одно из самых популярных приложений HTTP API — это реализация CRUD-интерфейсов. Акроним CRUD (**C**reate, **R**ead, **U**pdate, **D**elete) был популяризирован ещё в 1983 году Джеймсом Мартином, но с развитием HTTP API обрёл второе дыхание. Ключевая идея соответствия CRUD и HTTP заключается в том, что каждой из CRUD-операций соответствует один из глаголов HTTP: + * операции создания — создание ресурса через метод `POST`; + * операции чтения — возврат представления ресурса через метод `GET`; + * операции редактирования — перезапись ресурса через метод `PUT` или редактирование через `PATCH`; + * операции удаления — удаление ресурса через метод `DELETE`. + +Фактически, подобное соответствие — это просто мнемоническое правило, позволяющее определить, какой глагол следует использовать к какой операции. Мы, однако, должны предостеречь читателя: глагол следует выбирать по его семантике согласно стандарту, а не по мнемоническим правилам. Может показаться, что, например, операцию удаления 3-го элемента списка нужно реализовать через `DELETE`: + * `DELETE /v1/list/{list_id}/?position=3 HTTP 1.1` +но, как мы помним, делать так категорически нельзя: во-первых, такой вызов неидемпотентен; во-вторых, нарушает требование консистентности `GET` и `DELETE`. + +С точки зрения удобства разработки концепция выглядит очень удобной — каждому виду ресурсов соответствует свой URL, каждой операции — свой глагол. При пристальном рассмотрении, однако, оказывается, что отношение CRUD-операция / HTTP-глагол — очень упрощённое представление о манипуляции ресурсами, и, что самое неприятное, плохо расширяемое. + +##### Создание + +Начнём с операции создания ресурса. Как мы помним из предыдущих глав, операция создания в любой сколько-нибудь ответственной предметной области обязана быть идемпотентной и, очень желательно, ещё и позволять управлять параллелизмом. В рамках парадигмы HTTP API идемпотентное создание можно организовать одним из трёх способов: + + 1. Через метод `POST` с передачей токена идемпотентности (им может выступать, в частности, ревизия ресурса): + ``` + POST /v1/orders/?user_id= HTTP/1.1 + If-Match: <ревизия> + + { … } + ``` + + 2. Через метод `PUT`, предполагая, что идентификатор заказа сгенерирован клиентом (ревизия при этом всё ещё может использоваться для управления параллелизмом, но токеном идемпотентности является сам URL): + ``` + PUT /v1/orders/{order_id} HTTP/1.1 + If-Match: <ревизия> + + { … } + ``` + + 3. Через схему создания черновика методом `POST` и его подтверждения методом `PUT`: + ``` + POST /v1/drafts HTTP/1.1 + + { … } + → + HTTP/1.1 201 Created + Location: /v1/drafts/{id} + ``` + ``` + PUT /v1/drafts/{id}/status + If-Match: <ревизия> + + {"status": "confirmed"} + → + HTTP/1.1 200 Ok + Location: /v1/orders/{id} + ``` + +Метод (2) в современных системах используется редко, так как вынуждает доверять правильности генерации идентификатора заказа клиентом. Если же рассматривать варианты (1) и (3), то необходимо отметить, что семантике протокола вариант (3) соответствует лучше, так как `POST`-запросы по умолчанию считаются неидемпотентными, и их автоматический повтор в случае получения сетевого таймаута или ошибки сервера будет выглядеть для постороннего наблюдателя опасной операцией (которой запрос и правда может стать, если сервер изменит политику проверки заголовка `If-Match` на более мягкую). Повтор `PUT`-запроса (а мы предполагаем, что таймауты и серверные ошибки на «тяжёлой» операции создания заказа намного более вероятны, чем на «лёгкой» операции создания черновика) вполне может быть автоматизирован, и не будет создавать дубликаты заказа, даже если проверка ревизии будет отключена вообще. Однако теперь вместо двух URL и двух операций (`POST /v1/orders` — `GET /v1/orders/{id}`) мы имеем четыре URL и пять операций: + + 1. URL создания черновика(`POST /v1/drafts`), который дополнительно потребует существования URL самого черновика и/или списка черновиков пользователя (`GET /v1/drafts/?user_id=` или что-то аналогичное). + 2. URL подтверждения черновика (`PUT /v1/drafts/{id}/status`) и симметричную операцию чтения статуса черновика (через которую клиент должен будет получать актуальную ревизию для подтверждения черновика). + 3. URL заказа (`GET /v1/orders/{id}`). + +##### Чтение + +Идём дальше. Операция чтения на первый взгляд не вызывает сомнений: + * `GET /v1/orders/{id}` +…но это только на первый взгляд. Клиент как минимум должен обладать способом выяснить, какие заказы сейчас выполняются от его имени, что требует создания отдельного ресурса-поисковика: + * `GET /v1/orders/?user_id=` +…но передача списков без ограничений по их длине — потенциально плохая идея, а значит необходимо ввести поддержку пагинации: + * `GET /v1/orders/?user_id=&cursor=` +…но если заказов много, наверняка пользователю понадобятся фильтры, скажем, по названию напитка: + * `GET /v1/orders/?user_id=&recipe=lungo` +…но пользователь может захотеть видеть в одном списке латте и лунго: + * ??? + * общепринятого стандарта передачи в URL более сложных структур, чем пары ключ-значение, не существует. + +Довольно скоро мы придём к тому, что, наряду с доступом по идентификатору заказа потребуется ещё и, во-первых, способ строго перебрать все заказы и способ искать по нестрогому совпадению. + +Кроме того, если к заказу можно прикладывать какие-то медиа-данные (скажем, фотографии), то для доступа к ним придётся разработать отдельные URL: + * `GET /v1/orders/{order_id}/attachements/{id}` + +##### Редактирование + +Вопросы частичного редактирования мы подробно разбирали в соответствующей главе раздела «Паттерны API». Идея полной перезаписи ресурса методом `PUT` быстро разбивается о необходимость работать с вычисляемыми и неизменяемыми полями, необходимость совместного редактирования и/или большой объём передаваемых данных. Работа через метод `PATCH` возможна, но, так как этот метод по умолчанию считается неидемпотентным, для него справедливо всё то же соображение об опасности автоматических перезапросов. Достаточно быстро мы придём к одному из двух вариантов: + * либо `PUT` декомпозирован на множество составных `PUT /v1/orders/{id}/address`, `PUT /v1/orders/{id}/volume` и т.д. — по ресурсу для каждой частной операции; + * либо существует отдельный ресурс, принимающий список изменений, причём, вероятнее всего, через схему черновик-подтверждение. + +Если к сущности прилагаются медиаданные, для их редактирования также придётся разработать отдельные эндпойнты. + +##### Удаление + +С удалением ситуация проще всего: никакие данные в современных сервисах не удаляются моментально, а лишь архивируются или помечаются удалёнными. Таким образом, вместо `DELETE /v1/orders/{id}` необходимо разработать эндпойнт типа `PUT /v1/orders/{id}/archive` или `PUT /v1/archive?order=`. + +Таким образом, идея CRUD как способ минимальным набором операций описать типовые действия над ресурсом в при столкновении с реальностью быстро эволюционирует в сторону семейства эндпойнтов, каждый из которых описывает отдельный аспект взаимодействия с сущностью в течение её жизненного цикла. Изложенные выше соображения следует считать не критикой концепции CRUD как таковой, а скорее призывом не лениться и разрабатывать номенклатуру ресурсов и операций над ними исходя из конкретной предметной области, а не абстрактных мнемонических правил, к которым является эта концепция. Если вы всё же хотите разработать типовой API для манипуляции типовыми сущностями, стоит изначально разработать его гораздо более гибким, чем предлагает CRUD-HTTP методология. \ No newline at end of file diff --git a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/07.md b/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/07.md index 959c4f9..20da4f1 100644 --- a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/07.md +++ b/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/07.md @@ -1 +1 @@ -### Организация URL ресурсов и операций над ними в HTTP API \ No newline at end of file +### Работа с ошибками в HTTP API