You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-08-10 21:51:42 +02:00
CRUD
This commit is contained in:
BIN
docs/API.en.epub
BIN
docs/API.en.epub
Binary file not shown.
173
docs/API.en.html
173
docs/API.en.html
File diff suppressed because one or more lines are too long
BIN
docs/API.en.pdf
BIN
docs/API.en.pdf
Binary file not shown.
BIN
docs/API.ru.epub
BIN
docs/API.ru.epub
Binary file not shown.
165
docs/API.ru.html
165
docs/API.ru.html
File diff suppressed because one or more lines are too long
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
@@ -114,8 +114,8 @@
|
||||
<li><a href="API.en.html#http-api-requests-semantics">Chapter 35. Components of an HTTP Request and Their Semantics</a></li>
|
||||
<li><a href="API.en.html#http-api-pros-and-cons">Chapter 36. Advantages and Disadvantages of HTTP APIs</a></li>
|
||||
<li><a href="API.en.html#http-api-rest-organizing">Chapter 37. Organizing HTTP APIs Based on the REST Principles</a></li>
|
||||
<li><a href="API.en.html#chapter-38">Chapter 38. Working with HTTP API Errors</a></li>
|
||||
<li><a href="API.en.html#chapter-39">Chapter 39. Organizing the HTTP API Resources and Operations</a></li>
|
||||
<li><a href="API.en.html#http-api-urls-crud">Chapter 38. Designing a Nomenclature of URLs and Applicable Operations</a></li>
|
||||
<li><a href="API.en.html#chapter-39">Chapter 39. Working with HTTP API Errors</a></li>
|
||||
<li><a href="API.en.html#chapter-40">Chapter 40. Final Provisions and General Recommendations</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
@@ -114,8 +114,8 @@
|
||||
<li><a href="API.ru.html#http-api-requests-semantics">Глава 35. Составляющие HTTP запросов и их семантика</a></li>
|
||||
<li><a href="API.ru.html#http-api-pros-and-cons">Глава 36. Преимущества и недостатки HTTP API</a></li>
|
||||
<li><a href="API.ru.html#http-api-rest-organizing">Глава 37. Организация HTTP API согласно принципам REST</a></li>
|
||||
<li><a href="API.ru.html#chapter-38">Глава 38. Работа с ошибками в HTTP API</a></li>
|
||||
<li><a href="API.ru.html#chapter-39">Глава 39. Организация URL ресурсов и операций над ними в HTTP API</a></li>
|
||||
<li><a href="API.ru.html#http-api-urls-crud">Глава 38. Разработка номенклатуры URL ресурсов и операций над ними</a></li>
|
||||
<li><a href="API.ru.html#chapter-39">Глава 39. Работа с ошибками в HTTP API</a></li>
|
||||
<li><a href="API.ru.html#chapter-40">Глава 40. Заключительные положения и общие рекомендации</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
@@ -1,16 +1,16 @@
|
||||
### [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.
|
||||
As we noted on several occasions in the previous chapters, neither the HTTP and URL standards nor REST architectural principles prescribe concrete semantics for the meaningful parts of a URL (notably, path fragments and key-value pairs in the query). **The rules for organizing URLs in an HTTP API exist *only* to improve the API's readability and consistency from the developers' perspective**. However, this doesn't mean they are unimportant. Quite the opposite: URLs in HTTP APIs are a means of describing abstraction levels and entities' responsibility areas. A well-designed API hierarchy should be reflected in a well-designed URL nomenclature.
|
||||
|
||||
**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.
|
||||
**NB**: the lack of specific guidance from the specification editors naturally led to developers inventing it themselves. Many of these spontaneous practices can be found on the Internet, such as the requirement to use only nouns in URLs. They are often claimed to be a part of the standards or REST architectural principles (which they are not). Nevertheless, deliberately ignoring such self-proclaimed “best practices” is not the best decision for an API vendor as it increases the chances of being 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=<partner_id>`) or as a meaning to pass operation parameters (`/search/?recipe=lungo`).
|
||||
Traditionally, the following semantics are considered to be the default:
|
||||
* Path components (i.e., fragments between `/` symbols) are used to organize nested resources, such as `/partner/{id}/coffee-machines/{id}`. A path can be further extended by adding new suffixes to indicate subordinate sub-resources.
|
||||
* Query parameters are used to indicate non-strict connections (i.e., “many-to-many” relations such as `/recipes/?partner=<partner_id>`) or as a means 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:
|
||||
This convention allows for reflecting almost any API's entity nomenclature decently 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 interpreting 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?
|
||||
1. How exactly should the endpoints connecting two entities lacking a clear relation between them be organized? For example, how should a URL for preparing a lungo on a specific coffee machine look?
|
||||
* `/coffee-machines/{id}/recipes/lungo/prepare`
|
||||
* `/recipes/lungo/coffee-machines/{id}/prepare`
|
||||
* `/coffee-machines/{id}/prepare?recipe=lungo`
|
||||
@@ -18,12 +18,136 @@ This convention allows for decently reflecting almost any API's entity nomenclat
|
||||
* `/prepare?coffee_machine_id=<id>&recipe=lungo`
|
||||
* `/action=prepare&coffee_machine_id=<id>&recipe=lungo`
|
||||
|
||||
All these options are semantically viable and are generally speaking equitable.
|
||||
All these options are semantically viable and 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=<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.
|
||||
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 (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=<id>&recipe=lungo` is unacceptable at all as there is no object to act upon). However, this adds visual noise in the form of “ator” suffixes but definitely doesn't make the code more concise 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=<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.
|
||||
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 ambiguity in organizing APIs. Let's consider the `/v1/search` resource from our study API. Which verb should be used to request it?
|
||||
* On one hand, `GET /v1/search?query=<search query>` explicitly declares that there are no side effects (no state is overwritten) and the results can be cached (given that 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 dynamically formed from a plethora of various sources, and a subsequent request with the same query might yield 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?
|
||||
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 the results or hinting that the results are generated on the fly?
|
||||
|
||||
**NB**: the authors of the standard are also concerned about this dichotomy and have finally [proposed the `QUERY` HTTP method](https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html), which is basically a safe (i.e., non-modifying) version of `POST`. However, we do not expect it to gain widespread adoption just as [the existing `SEARCH` verb](https://www.rfc-editor.org/rfc/rfc5323) did not.
|
||||
|
||||
Unfortunately, we don't have simple answers to these questions. In this book, we stick to the following approach:
|
||||
* Signatures must be readable and concise first and foremost. Making the code more complicated to align with some abstract concept is undesirable.
|
||||
* Hierarchies are indicated if they are unequivocal. If a low-level entity is a full subordinate of a higher-level entity, the relation will be expressed with nested path fragments.
|
||||
* If there are doubts about the hierarchy persisting during further development of the API, it is more convenient to create a new root path prefix rather than employ nested paths.
|
||||
* For “cross-domain” operations (i.e., when it is necessary to refer to entities of different abstraction levels within one request) it is better to have a dedicated resource specifically for this operation (e.g., in the example above, we would prefer the `/prepare?coffee_machine_id=<id>&recipe=lungo` signature).
|
||||
* The semantics of the HTTP verbs take priority over false non-safety / non-idempotency warnings. Furthermore, the author of this book prefers using `POST` to indicate any unexpected side effects of an operation, such as high computational complexity, even if it is fully safe.
|
||||
|
||||
**NB**: passing variables as either query parameters or path fragments affects not only readability. Let's consider the example from the previous chapter and imagine that gateway D is implemented as a stateless proxy with a declarative configuration. Then receiving a request like this:
|
||||
* `GET /v1/state?user_id=<user_id>`
|
||||
|
||||
and transforming it into a pair of nested sub-requests:
|
||||
|
||||
* `GET /v1/profiles?user_id=<user_id>`
|
||||
* `GET /v1/orders?user_id=<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, the automated creation of monitoring panels in services like the Prometheus+Grafana bundle (or basically any other log analyzing tool) is much easier to organize by path prefix than by a synthetic key computed from request parameters.
|
||||
|
||||
All this leads us to the conclusion that maintaining an identical URL structure when paths are fixed and all the parameters are passed as query parameters will result in an even more uniform interface, although less readable and semantic. In internal systems, preferring the convenience of usage over readability is sometimes an obvious decision. In public APIs, we would rather discourage implementing this approach.
|
||||
|
||||
#### The CRUD Operations
|
||||
|
||||
One of the most popular tasks solved by exposing HTTP APIs is implementing CRUD interfaces. The “CRUD” acronym (which stands for **C**reate, **R**ead, **U**pdate, **D**elete) was popularized in 1983 by James Martin and gained a second wind with HTTP APIs gaining widespread acclaim. The key concept is that every CRUD operation matches a specific HTTP verb:
|
||||
* The “create” operation corresponds to the HTTP `POST` method.
|
||||
* The “read” operation corresponds to returning a representation of the resource via the `GET` method.
|
||||
* The “update” operation corresponds to overwriting a resource with either the `PUT` or `PATCH` method.
|
||||
* The “delete” operation corresponds to deleting a resource with the `DELETE` method.
|
||||
|
||||
**NB**: in fact, this correspondence serves as a mnemonic to choose the appropriate HTTP verb for each operation. However, we must warn the reader that verbs should be chosen according to their definition in the standards, not based on mnemonic rules. For example, it might seem like deleting the third element in a list should be organized via the `DELETE` method:
|
||||
* `DELETE /v1/list/{list_id}/?position=3`
|
||||
|
||||
However, as we remember, doing so is a grave mistake: first, such a call is non-idempotent, and second, it violates the `GET` / `DELETE` consistency principle.
|
||||
|
||||
The CRUD/HTTP correspondence might appear convenient as every resource is forced to have its own URL and each operation has a suitable verb. However, upon closer examination, we will quickly understand that the correspondence presents resource manipulation in a very simplified, and, even worse, poorly extensible way.
|
||||
|
||||
##### Creating
|
||||
|
||||
Let's start with the resource creation operation. As we remember from the “[Synchronization Strategies](#api-patterns-sync-strategies)” chapter, in any important subject area, creating entities must be an idempotent procedure that ideally allows for controlling concurrency. In the HTTP API paradigm, idempotent creation could be implemented using one of the following three approaches:
|
||||
1. Through the `POST` method with passing an idempotency token (in which capacity the resource `ETag` might be employed):
|
||||
```
|
||||
POST /v1/orders/?user_id=<user_id> HTTP/1.1
|
||||
If-Match: <revision>
|
||||
|
||||
{ … }
|
||||
```
|
||||
|
||||
2. Through the `PUT` method, implying that the entity identifier is generated by the client. Revision still could be used for controlling concurrency; however, the idempotency token is the URL itself:
|
||||
```
|
||||
PUT /v1/orders/{order_id} HTTP/1.1
|
||||
If-Match: <revision>
|
||||
|
||||
{ … }
|
||||
```
|
||||
|
||||
|
||||
3. By creating a draft with the `POST` method and then committing it with the `PUT` method:
|
||||
```
|
||||
POST /v1/drafts HTTP/1.1
|
||||
|
||||
{ … }
|
||||
→
|
||||
HTTP/1.1 201 Created
|
||||
Location: /v1/drafts/{id}
|
||||
```
|
||||
```
|
||||
PUT /v1/drafts/{id}/commit
|
||||
If-Match: <revision>
|
||||
|
||||
{"status": "confirmed"}
|
||||
→
|
||||
HTTP/1.1 200 OK
|
||||
Location: /v1/orders/{id}
|
||||
```
|
||||
|
||||
Approach \#2 is rarely used in modern systems as it requires trusting the client to generate identifiers properly. If we consider options \#1 and \#3, we must note that the latter conforms to HTTP semantics better as `POST` requests are considered non-idempotent by default and should not be repeated in case of a timeout or server error. Therefore, repeating a request would appear as a mistake from an external observer's perspective, and it could indeed become one if the server changes the `If-Match` header check policy to a more relaxed one. Conversely, repeating a `PUT` request (assuming that getting a timeout or an error while performing a “heavy” order creation operation is much more probable than in the case of a “lightweight” draft creation) could be automated and would not result in order duplication even if the revision check is disabled. However, instead of two URLs and two operations (`POST /v1/orders` / `GET /v1/orders/{id}`), we now have four URLs and five operations:
|
||||
|
||||
1. The draft creation URL (`POST /v1/drafts`), which additionally requires a method of retrieving pending drafts through something like `GET /v1/drafts/?user_id=<user_id>`.
|
||||
|
||||
2. The URL to confirm a draft, and perhaps the symmetrical operation of getting draft status (though the `GET /drafts` resource mentioned above might serve this purpose as well).
|
||||
|
||||
3. The URL of an order (`GET /v1/orders/{id}`).
|
||||
|
||||
##### Reading
|
||||
|
||||
Let's continue. The reading operation is at first glance straightforward:
|
||||
* `GET /v1/orders/{id}`.
|
||||
|
||||
However, upon closer inspection, it becomes less simple. First, the client should have a method to retrieve the ongoing orders executed on behalf of the user, which requires creating a separate enumerator resource:
|
||||
* `GET /v1/orders/?user_id=<user_id>`.
|
||||
|
||||
Returning potentially long lists in a single response is a bad idea, so we will need pagination:
|
||||
* `GET /v1/orders/?user_id=<user_id>&cursor=<cursor>`.
|
||||
|
||||
If there is a long list of orders, the user will require filters to navigate it. Let's say we introduce a beverage type filter:
|
||||
* `GET /v1/orders/?user_id=<user_id>&recipe=lungo`.
|
||||
|
||||
However, if the user needs to see a single list containing both latte and lungo orders, this interface becomes much less viable as there is no universally adopted technique for passing structures in that are more complex than key-value pairs. Soon, we will face the need to have a search endpoint with rich semantics, which naturally should be represented as a `POST` request body.
|
||||
|
||||
Additionally, if some media data could be attached to an order (such as photos), a separate endpoint to expose them should be developed:
|
||||
* `GET /v1/orders/{order_id}/attachments/{id}`.
|
||||
|
||||
##### Updating
|
||||
|
||||
The problem of partial updates was discussed in detail in the [corresponding chapter](#api-patterns-partial-updates) of “The API Patterns” section. To quickly recap:
|
||||
* The concept of fully overwriting resources with `PUT` is viable but soon faces problems when working with calculated or immutable fields and organizing collaborative editing. It is also suboptimal in terms of traffic consumption.
|
||||
* Partially updating a resource using the `PATCH` method is potentially non-idempotent (and likely non-transitive), and the aforementioned concerns regarding automatic retries are applicable to it as well.
|
||||
|
||||
If we need to update a complex entity, especially if collaborative editing is needed, we will soon find ourselves leaning towards one of the following two approaches:
|
||||
* Decomposing the `PUT` functionality into a set of atomic nested handlers (like `PUT /v1/orders/{id}/address`, `PUT /v1/orders/{id}/volume`, etc.), one for each specific operation.
|
||||
* Introducing a resource to process a list of changes encoded in a specially designed format. Likely, this resource will also require implementing a draft/commit scheme via a `POST` + `PUT` pair of methods.
|
||||
|
||||
If media data is attached to an entity, we will additionally require more endpoints to amend this metadata.
|
||||
|
||||
##### Deleting
|
||||
|
||||
Finally, with deleting resources the situation is simple: in modern services, data is never deleted, only archived or marked as deleted. Therefore, instead of a `DELETE /v1/orders/{id}` endpoint there should be `PUT /v1/orders/{id}/archive` or `PUT /v1/archive?order=<order_id>`.
|
||||
|
||||
#### In Conclusion
|
||||
|
||||
The idea of CRUD as a methodology of describing typical operations applied to resources with a small set of uniform verbs quickly evolves towards a bucket of different endpoints, each of them covering some specific aspect of working with the entity during its lifecycle.
|
||||
|
||||
This discourse is not to be perceived as criticizing the idea of CRUD itself. We just point out that in complex subject areas cutting edges and sticking to some mnemonic rules rarely plays out. It is much better to design entity manipulation URLs based on specific use cases. And if you do want to have a uniform interface to manipulate typical entities, you would rather initially design it much more detailed and extensible than just a set of four HTTP-CRUD methods.
|
@@ -28,15 +28,17 @@
|
||||
|
||||
Иными словами, для любых операций, результат которых представляет собой результат работы какого-то алгоритма (например, список релевантных предложений по запросу) мы всегда будем сталкиваться с выбором, что важнее: семантика глагола или отсутствие побочных эффектов? Кэширование ответа или индикация того, что операция вычисляет результаты на лету?
|
||||
|
||||
Простых ответов на вопросы выше у нас, к сожалению, нет (особенно если мы добавим к ним механики логирования и построения мониторингов по URL запроса). В рамках настоящей книги мы придерживаемся следующего подхода:
|
||||
**NB**: эта дихотомия волнует не только нас, но и авторов стандарта, которые в конечном итоге [предложили новый глагол `QUERY`](https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html), который по сути является немодифицирующим `POST`. Мы, однако, сомневаемся, что он получит широкое распространение — поскольку [уже существующий `SEARCH`](https://www.rfc-editor.org/rfc/rfc5323) оказался в этом качестве никому не нужен.
|
||||
|
||||
Простых ответов на вопросы выше у нас, к сожалению, нет. В рамках настоящей книги мы придерживаемся следующего подхода:
|
||||
|
||||
* сигнатура вызова в первую очередь должна быть лаконична и читабельна; усложнение сигнатур в угоду абстрактным концепциям нежелательно;
|
||||
* иерархия ресурсов выдерживается там, где она однозначна (т.е., если сущность низшего уровня абстракции однозначно подчинена сущности высшего уровня абстракции, то отношения между ними будут выражены в виде вложенных путей);
|
||||
* если есть сомнения в том, что иерархия в ходе дальнейшего развития API останется неизменной, лучше завести новый верхнеуровневый префикс, а не вкладывать новые сущности в уже существующие;
|
||||
* семантика HTTP-глагола приоритетнее ложного предупреждения о небезопасности/неидемпотентности (в частности, если операция является безопасной, но ресурсозатратной, с нашей точки зрения вполне разумно использовать метод `POST` для индикации этого факта);
|
||||
* для выполнения кросс-доменных операций предпочтительнее завести специальный ресурс, выполняющий операцию (т.е. в примере с кофе-машинами и рецептами автор этой книги выбрал бы вариант `/prepare?coffee_machine_id=<id>&recipe=lungo`).
|
||||
* для выполнения «кросс-доменных» операций (т.е. при необходимости сослаться на объекты разных уровней абстракции в одном вызове) предпочтительнее завести специальный ресурс, выполняющий операцию (т.е. в примере с кофе-машинами и рецептами автор этой книги выбрал бы вариант `/prepare?coffee_machine_id=<id>&recipe=lungo`);
|
||||
* семантика HTTP-глагола приоритетнее ложного предупреждения о небезопасности/неидемпотентности (в частности, если операция является безопасной, но ресурсозатратной, с нашей точки зрения вполне разумно использовать метод `POST` для индикации этого факта).
|
||||
|
||||
**NB**: отметим, что передача параметров в виде пути или query-параметра в URL влияет не только на читабельность. Если представить, что гейтвей D реализован в виде stateless прокси с декларативной конфигурацией, то получать от клиента запрос в виде:
|
||||
**NB**: отметим, что передача параметров в виде пути или query-параметра в URL влияет не только на читабельность. Вернёмся к примеру из предыдущей главы и представим, что гейтвей D реализован в виде stateless прокси с декларативной конфигурацией. Тогда получать от клиента запрос в виде:
|
||||
* `GET /v1/state?user_id=<user_id>`
|
||||
|
||||
и преобразовывать в пару вложенных запросов
|
||||
@@ -44,22 +46,10 @@
|
||||
* `GET /v1/profiles?user_id=<user_id>`
|
||||
* `GET /v1/orders?user_id=<user_id>`
|
||||
|
||||
гораздо удобнее, чем извлекать идентификатор из path и преобразовывать его в query-параметр. Первую операцию [замена одного path целиком на другой] достаточно просто описать декларативно, и в большинстве ПО для веб-серверов она поддерживается из коробки. Напротив, извлечение данных из разных компонентов и полная пересборка запроса — достаточно сложная функциональность, которая, скорее всего, потребует от гейтвея поддержки скриптового языка программирования и/или написания специального модуля для таких манипуляций. Аналогично, автоматическое построение мониторинговых панелей в популярных сервисах типа связки Prometheus+Grafana гораздо проще организовать по path, чем вычленять из данных запроса какой-то синтетический ключ группировки запросов.
|
||||
гораздо удобнее, чем извлекать идентификатор из 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=<user_id>`
|
||||
|
||||
and transforming it into a pair of nested sub-requests:
|
||||
|
||||
* `GET /v1/profiles?user_id=<user_id>`
|
||||
* `GET /v1/orders?user_id=<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:
|
||||
@@ -68,17 +58,18 @@
|
||||
* операции редактирования — перезапись ресурса через метод `PUT` или редактирование через `PATCH`;
|
||||
* операции удаления — удаление ресурса через метод `DELETE`.
|
||||
|
||||
Фактически, подобное соответствие — это просто мнемоническое правило, позволяющее определить, какой глагол следует использовать к какой операции. Мы, однако, должны предостеречь читателя: глагол следует выбирать по его семантике согласно стандарту, а не по мнемоническим правилам. Может показаться, что, например, операцию удаления 3-го элемента списка нужно реализовать через `DELETE`:
|
||||
* `DELETE /v1/list/{list_id}/?position=3 HTTP 1.1`
|
||||
но, как мы помним, делать так категорически нельзя: во-первых, такой вызов неидемпотентен; во-вторых, нарушает требование консистентности `GET` и `DELETE`.
|
||||
**NB**: фактически, подобное соответствие — это просто мнемоническое правило, позволяющее определить, какой глагол следует использовать к какой операции. Мы, однако, должны предостеречь читателя: глагол следует выбирать по его семантике согласно стандарту, а не по мнемоническим правилам. Может показаться, что, например, операцию удаления 3-го элемента списка нужно реализовать через `DELETE`:
|
||||
* `DELETE /v1/list/{list_id}/?position=3`
|
||||
|
||||
С точки зрения удобства разработки концепция выглядит очень удобной — каждому виду ресурсов соответствует свой URL, каждой операции — свой глагол. При пристальном рассмотрении, однако, оказывается, что отношение CRUD-операция / HTTP-глагол — очень упрощённое представление о манипуляции ресурсами, и, что самое неприятное, плохо расширяемое.
|
||||
но, как мы помним, делать так категорически нельзя: во-первых, такой вызов неидемпотентен; во-вторых, нарушает требование консистентности `GET` и `DELETE`.
|
||||
|
||||
С точки зрения удобства разработки концепция соответствия CRUD и HTTP выглядит очень удобной — каждому виду ресурсов соответствует свой URL, каждой операции — свой глагол. При пристальном рассмотрении, однако, оказывается, что это отношение — очень упрощённое представление о манипуляции ресурсами, и, что самое неприятное, плохо расширяемое.
|
||||
|
||||
##### Создание
|
||||
|
||||
Начнём с операции создания ресурса. Как мы помним из предыдущих глав, операция создания в любой сколько-нибудь ответственной предметной области обязана быть идемпотентной и, очень желательно, ещё и позволять управлять параллелизмом. В рамках парадигмы HTTP API идемпотентное создание можно организовать одним из трёх способов:
|
||||
Начнём с операции создания ресурса. Как мы помним из главы «[Стратегии синхронизации](#api-patterns-sync-strategies)”, операция создания в любой сколько-нибудь ответственной предметной области обязана быть идемпотентной и, очень желательно, ещё и позволять управлять параллелизмом. В рамках парадигмы HTTP API идемпотентное создание можно организовать одним из трёх способов:
|
||||
|
||||
1. Через метод `POST` с передачей токена идемпотентности (им может выступать, в частности, ревизия ресурса):
|
||||
1. Через метод `POST` с передачей токена идемпотентности (им может выступать, в частности, `ETag` ресурса):
|
||||
```
|
||||
POST /v1/orders/?user_id=<user_id> HTTP/1.1
|
||||
If-Match: <ревизия>
|
||||
@@ -104,45 +95,46 @@
|
||||
Location: /v1/drafts/{id}
|
||||
```
|
||||
```
|
||||
PUT /v1/drafts/{id}/status
|
||||
PUT /v1/drafts/{id}/commit
|
||||
If-Match: <ревизия>
|
||||
|
||||
{"status": "confirmed"}
|
||||
→
|
||||
HTTP/1.1 200 Ok
|
||||
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=<user_id>` или что-то аналогичное).
|
||||
2. URL подтверждения черновика (`PUT /v1/drafts/{id}/status`) и симметричную операцию чтения статуса черновика (через которую клиент должен будет получать актуальную ревизию для подтверждения черновика).
|
||||
1. URL создания черновика (`POST /v1/drafts`), который дополнительно потребует существования URL последнего черновика и/или списка черновиков пользователя (`GET /v1/drafts/?user_id=<user_id>` или что-то аналогичное).
|
||||
2. URL подтверждения черновика (`PUT /v1/drafts/{id}/status`) и, может быть, симметричную операцию чтения статуса черновика для получения актуальной ревизии (хотя эндпойнт `GET /v1/drafts`, описанный выше, для этого подходит лучше).
|
||||
3. URL заказа (`GET /v1/orders/{id}`).
|
||||
|
||||
##### Чтение
|
||||
|
||||
Идём дальше. Операция чтения на первый взгляд не вызывает сомнений:
|
||||
* `GET /v1/orders/{id}`
|
||||
…но это только на первый взгляд. Клиент как минимум должен обладать способом выяснить, какие заказы сейчас выполняются от его имени, что требует создания отдельного ресурса-поисковика:
|
||||
* `GET /v1/orders/?user_id=<user_id>`
|
||||
…но передача списков без ограничений по их длине — потенциально плохая идея, а значит необходимо ввести поддержку пагинации:
|
||||
* `GET /v1/orders/?user_id=<user_id>&cursor=<cursor>`
|
||||
…но если заказов много, наверняка пользователю понадобятся фильтры, скажем, по названию напитка:
|
||||
* `GET /v1/orders/?user_id=<user_id>&recipe=lungo`
|
||||
…но пользователь может захотеть видеть в одном списке латте и лунго:
|
||||
* ???
|
||||
* общепринятого стандарта передачи в URL более сложных структур, чем пары ключ-значение, не существует.
|
||||
* `GET /v1/orders/{id}`.
|
||||
|
||||
Довольно скоро мы придём к тому, что, наряду с доступом по идентификатору заказа потребуется ещё и, во-первых, способ строго перебрать все заказы и способ искать по нестрогому совпадению.
|
||||
Стоит, однако, присмотреться внимательнее, и всё оказывается не так просто. Клиент как минимум должен обладать способом выяснить, какие заказы сейчас выполняются от его имени, что требует создания отдельного ресурса-поисковика:
|
||||
* `GET /v1/orders/?user_id=<user_id>`.
|
||||
|
||||
Передача списков без ограничений по их длине — потенциально плохая идея, а значит необходимо ввести поддержку пагинации:
|
||||
* `GET /v1/orders/?user_id=<user_id>&cursor=<cursor>`.
|
||||
|
||||
Если заказов много, наверняка пользователю понадобятся фильтры, скажем, по названию напитка:
|
||||
* `GET /v1/orders/?user_id=<user_id>&recipe=lungo`.
|
||||
|
||||
Однако, если пользователь захочет видеть в одном списке и латте и лунго, этот интерфейс уже окажется ограниченно применимым, поскольку общепринятого стандарта передачи в URL более сложных структур, чем пары ключ-значение, не существует. Довольно скоро мы придём к тому, что, наряду с доступом по идентификатору заказа потребуется ещё и поисковый эндпойнт со сложной семантикой (которую гораздо удобнее было бы разместить за `POST`):
|
||||
* `POST /v1/orders/search { /* parameters */ }`
|
||||
|
||||
Кроме того, если к заказу можно прикладывать какие-то медиа-данные (скажем, фотографии), то для доступа к ним придётся разработать отдельные URL:
|
||||
* `GET /v1/orders/{order_id}/attachements/{id}`
|
||||
|
||||
##### Редактирование
|
||||
|
||||
Вопросы частичного редактирования мы подробно разбирали в соответствующей главе раздела «Паттерны API». Идея полной перезаписи ресурса методом `PUT` быстро разбивается о необходимость работать с вычисляемыми и неизменяемыми полями, необходимость совместного редактирования и/или большой объём передаваемых данных. Работа через метод `PATCH` возможна, но, так как этот метод по умолчанию считается неидемпотентным, для него справедливо всё то же соображение об опасности автоматических перезапросов. Достаточно быстро мы придём к одному из двух вариантов:
|
||||
Проблемы частичного обновления ресурсов мы подробно разбирали в [соответствующей главе](#api-patterns-partial-updates) раздела «Паттерны дизайна API». Напомним, что полная перезапись ресурса методом `PUT` возможна, но быстро разбивается о необходимость работать с вычисляемыми и неизменяемыми полями, необходимость совместного редактирования и/или большой объём передаваемых данных. Работа через метод `PATCH` возможна, но, так как этот метод по умолчанию считается неидемпотентным (и часто нетразитивным), для него справедливо всё то же соображение об опасности автоматических перезапросов. Достаточно быстро мы придём к одному из двух вариантов:
|
||||
* либо `PUT` декомпозирован на множество составных `PUT /v1/orders/{id}/address`, `PUT /v1/orders/{id}/volume` и т.д. — по ресурсу для каждой частной операции;
|
||||
* либо существует отдельный ресурс, принимающий список изменений, причём, вероятнее всего, через схему черновик-подтверждение.
|
||||
* либо существует отдельный ресурс, принимающий список изменений, причём, вероятнее всего, через схему черновик-подтверждение в виде пары методов `POST` + `PUT`.
|
||||
|
||||
Если к сущности прилагаются медиаданные, для их редактирования также придётся разработать отдельные эндпойнты.
|
||||
|
||||
@@ -150,4 +142,8 @@
|
||||
|
||||
С удалением ситуация проще всего: никакие данные в современных сервисах не удаляются моментально, а лишь архивируются или помечаются удалёнными. Таким образом, вместо `DELETE /v1/orders/{id}` необходимо разработать эндпойнт типа `PUT /v1/orders/{id}/archive` или `PUT /v1/archive?order=<order_id>`.
|
||||
|
||||
Таким образом, идея CRUD как способ минимальным набором операций описать типовые действия над ресурсом в при столкновении с реальностью быстро эволюционирует в сторону семейства эндпойнтов, каждый из которых описывает отдельный аспект взаимодействия с сущностью в течение её жизненного цикла. Изложенные выше соображения следует считать не критикой концепции CRUD как таковой, а скорее призывом не лениться и разрабатывать номенклатуру ресурсов и операций над ними исходя из конкретной предметной области, а не абстрактных мнемонических правил, к которым является эта концепция. Если вы всё же хотите разработать типовой API для манипуляции типовыми сущностями, стоит изначально разработать его гораздо более гибким, чем предлагает CRUD-HTTP методология.
|
||||
#### В качестве заключения
|
||||
|
||||
Идея CRUD как способ минимальным набором операций описать типовые действия над ресурсом в при столкновении с реальностью быстро эволюционирует в сторону семейства эндпойнтов, каждый из которых описывает отдельный аспект взаимодействия с сущностью в течение её жизненного цикла.
|
||||
|
||||
Изложенные выше соображения следует считать не критикой концепции CRUD как таковой, а скорее призывом не лениться и разрабатывать номенклатуру ресурсов и операций над ними исходя из конкретной предметной области, а не абстрактных мнемонических правил, к которым является эта концепция. Если вы всё же хотите разработать типовой API для манипуляции типовыми сущностями, стоит изначально разработать его гораздо более гибким, чем предлагает CRUD-HTTP методология.
|
Reference in New Issue
Block a user