mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-05-31 22:09:37 +02:00
formatting
This commit is contained in:
parent
537be044d8
commit
8570c49a6e
@ -425,7 +425,7 @@ Returning to our example, how would retrieving the order status work? To obtain
|
||||
* The `runs` endpoint completes operations corresponding to its level (e.g., checks the coffee machine API kind) and, depending on the API kind, proceeds with one of two possible execution branches:
|
||||
* Either calls the `GET /execution/status` method of the physical coffee machine API, gets the coffee volume, and compares it to the reference value or
|
||||
* Invokes the `GET /v1/runtimes/{runtime_id}` method to obtain the `state.status` and converts it to the order status.
|
||||
* In the case of the second-kind API, the call chain continues: the `GET /runtimes` handler invokes the `GET /sensors` method of the physical coffee machine API and performs some manipulations with the data, like comparing the cup / ground coffee / shed water volumes with the reference ones, and changing the state and the status if needed.
|
||||
* In the case of the second-kind API, the call chain continues: the `GET /runtimes` handler invokes the `GET /sensors` method of the physical coffee machine API and performs some manipulations with the data, like comparing the cup / ground coffee / shed water volumes with the reference ones, and changing the state and the status if needed.
|
||||
|
||||
**NB**: The term “call chain” shouldn't be taken literally. Each abstraction level may be organized differently in a technical sense. For example:
|
||||
* There might be explicit proxying of calls down the hierarchy
|
||||
@ -468,7 +468,7 @@ This exercise doesn't just help but also allows us design really large APIs with
|
||||
|
||||
What data flow do we have in our coffee API?
|
||||
|
||||
1. It starts with the sensors data, e.g., volumes of coffee / water / cups. This is the lowest data level we have, and here we can't change anything.
|
||||
1. It starts with the sensors data, e.g., volumes of coffee / water / cups. This is the lowest data level we have, and here we can't change anything.
|
||||
|
||||
2. A continuous sensors data stream is being transformed into discrete command execution statuses, injecting new concepts which don't exist within the subject area. A coffee machine API doesn't provide a “coffee is being poured” or a “cup is being set” notion. It's our software that treats incoming sensor data and introduces new terms: if the volume of coffee or water is less than the target one, then the process isn't over yet. If the target value is reached, then this synthetic status is to be switched, and the next command is executed.
|
||||
It is important to note that we don't calculate new variables out of sensor data: we need to create a new dataset first, a context, an “execution program” comprising a sequence of steps and conditions, and fill it with initial values. If this context is missing, it's impossible to understand what's happening with the machine.
|
||||
|
@ -14,7 +14,7 @@ Rules are simply formulated generalizations based on one's experience. They are
|
||||
|
||||
This idea applies to every concept listed below. If you end up with an unusable, bulky, or non-obvious API because you followed the rules, it's a motivation to revise the rules (or the API).
|
||||
|
||||
It is important to understand that you can always introduce your own concepts. For example, some frameworks intentionally reject paired `set_entity` / `get_entity` methods in favor of a single `entity()` method with an optional argument. The crucial part is being systematic in applying the concept. If it is implemented, you must apply it to every single API method or at the very least develop a naming rule to distinguish such polymorphic methods from regular ones.
|
||||
It is important to understand that you can always introduce your own concepts. For example, some frameworks intentionally reject paired `set_entity` / `get_entity` methods in favor of a single `entity()` method with an optional argument. The crucial part is being systematic in applying the concept. If it is implemented, you must apply it to every single API method or at the very least develop a naming rule to distinguish such polymorphic methods from regular ones.
|
||||
|
||||
##### Explicit Is Always Better Than Implicit
|
||||
|
||||
@ -162,10 +162,10 @@ GET /v1/coffee-machines/{id}⮠
|
||||
|
||||
##### Matching Entities Must Have Matching Names and Behave Alike
|
||||
|
||||
**Bad**: `begin_transition` / `stop_transition`
|
||||
**Bad**: `begin_transition` / `stop_transition`
|
||||
— The terms `begin` and `stop` don't match; developers will have to refer to the documentation to find a paired method.
|
||||
|
||||
**Better**: either `begin_transition` / `end_transition` or `start_transition` / `stop_transition`.
|
||||
**Better**: either `begin_transition` / `end_transition` or `start_transition` / `stop_transition`.
|
||||
|
||||
**Bad**:
|
||||
```
|
||||
|
@ -57,7 +57,7 @@ Such a token might be:
|
||||
* An identifier (or identifiers) of the last modifying operations carried out by the client
|
||||
* The last known resource version (modification date, ETag) known to the client.
|
||||
|
||||
Upon getting the token, the server must check that the response (e.g., the list of ongoing operations it returns) matches the token, i.e., the eventual consistency converged. If it did not (the client passed the modification date / version / last order id newer than the one known to the server), one of the following policies or their combinations might be applied:
|
||||
Upon getting the token, the server must check that the response (e.g., the list of ongoing operations it returns) matches the token, i.e., the eventual consistency converged. If it did not (the client passed the modification date / version / last order id newer than the one known to the server), one of the following policies or their combinations might be applied:
|
||||
* The server might repeat the request to the underlying DB or to the other kind of data storage in order to get the newest version (eventually)
|
||||
* The server might return an error that requires the client to try again later
|
||||
* The server queries the main node of the DB, if such a thing exists, or otherwise initiates retrieving the master data.
|
||||
@ -100,7 +100,7 @@ Let's now imagine that we dropped the third requirement — i.e., returning the
|
||||
|
||||
Mathematically, the probability of getting the error is expressed quite simply. It's the ratio between two durations: the time period needed to get the actual state to the time period needed to restart the app and repeat the request. (Keep in mind that the last failed request might be automatically repeated on startup by the client.) The former depends on the technical properties of the system (for instance, on the replication latency, i.e., the lag between the master and its read-only copies) while the latter depends on what client is repeating the call.
|
||||
|
||||
If we talk about applications for end users, the typical restart time there is measured in seconds, which normally should be much less than the overall replication latency. Therefore, client errors will only occur in case of data replication problems / network issues / server overload.
|
||||
If we talk about applications for end users, the typical restart time there is measured in seconds, which normally should be much less than the overall replication latency. Therefore, client errors will only occur in case of data replication problems / network issues / server overload.
|
||||
|
||||
If, however, we talk about server-to-server applications, the situation is totally different: if a server repeats the request after a restart (let's say because the process was killed by a supervisor), it's typically a millisecond-scale delay. And that means that the number of order creation errors will be significant.
|
||||
|
||||
|
@ -293,7 +293,7 @@ POST /v1/partners/{id}/offers/history⮠
|
||||
|
||||
A small footnote: sometimes, the absence of the next-page cursor in the response is used as a flag to signal that iterating is over and there are no more elements in the list. However, we would rather recommend not using this practice and always returning a cursor even if it points to an empty page. This approach allows for adding the functionality of dynamically inserting new items at the end of the list.
|
||||
|
||||
**NB**: in some articles, organizing list traversals through monotonous identifiers / creation dates / cursors is not recommended because it is impossible to show a page selection to the end user and allow them to choose the desired result page. However, we should consider the following:
|
||||
**NB**: in some articles, organizing list traversals through monotonous identifiers / creation dates / cursors is not recommended because it is impossible to show a page selection to the end user and allow them to choose the desired result page. However, we should consider the following:
|
||||
* This case, of showing a pager and selecting a page, makes sense for end-user interfaces only. It's unlikely that an API would require access to random data pages.
|
||||
* If we talk about the internal API for an application that provides the UI control element with a pager, the proper approach is to prepare the data for this control element on the server side, including generating links to pages.
|
||||
* The boundary-based approach doesn't mean that using `limit`/`offset` parameters is prohibited. It is quite possible to have a double interface that would respond to both `GET /items?cursor=…` and `GET /items?offset=…&limit=…` queries.
|
||||
|
@ -52,7 +52,7 @@ There is also a Web standard for sending server notifications called [Server-Sen
|
||||
|
||||
##### Third-Party Push Notifications
|
||||
|
||||
One of the notorious problems with the long polling / WebSocket / SSE / MQTT technologies is the necessity to maintain an open network connection between the client and the server, which might be a problem for mobile applications and IoT devices from in terms of performance and battery life. One option that allows for mitigating the issue is delegating sending push notifications to a third-party service (the most popular choice today is Google's Firebase Cloud Messaging) that delivers notifications through the built-in mechanisms of the platform. Using such integrated services takes most of the load of maintaining open connections and checking their status off the developer's shoulders. The disadvantages of using third-party services are the necessity to pay for them and strict limits on message sizes.
|
||||
One of the notorious problems with the long polling / WebSocket / SSE / MQTT technologies is the necessity to maintain an open network connection between the client and the server, which might be a problem for mobile applications and IoT devices from in terms of performance and battery life. One option that allows for mitigating the issue is delegating sending push notifications to a third-party service (the most popular choice today is Google's Firebase Cloud Messaging) that delivers notifications through the built-in mechanisms of the platform. Using such integrated services takes most of the load of maintaining open connections and checking their status off the developer's shoulders. The disadvantages of using third-party services are the necessity to pay for them and strict limits on message sizes.
|
||||
|
||||
Also, sending push notifications to end-user devices suffers from one important issue: the percentage of successfully delivered messages never reaches 100%; the message drop rate might be tens of percent. Taking into account the message size limitations, it's actually better to implement a mixed model than a pure push model: the client continues polling the server, just less frequently, and push notifications just trigger ahead-of-time polling. (This problem is actually applicable to any notification delivery technology. Low-level protocols offer more options to set delivery guarantees; however, given the situation with forceful closing of open connections by OSes, having low-frequency polling as a precaution in an application is almost never a bad thing.)
|
||||
|
||||
|
@ -105,7 +105,7 @@ registerProgramRunHandler(
|
||||
**NB**: In the case of HTTP API, a corresponding example would look rather bulky as it would require implementing several additional endpoints for the message exchange like `GET /program-run/events` and `GET /partner/{id}/execution/events`. We would leave this exercise to the reader. Also, it's worth mentioning that in real-world systems such event queues are usually organized using external event messaging systems like Apache Kafka or Amazon SNS/SQS.
|
||||
|
||||
At this point, a mindful reader might begin protesting because if we take a look at the nomenclature of the new entities, we will find that nothing changed in the problem statement. It actually became even more complicated:
|
||||
* Instead of calling the `takeout` method, we're now generating a pair of `takeout_requested` / `takeout_ready` events
|
||||
* Instead of calling the `takeout` method, we're now generating a pair of `takeout_requested` / `takeout_ready` events
|
||||
* Instead of a long list of methods that shall be implemented to integrate the partner's API, we now have a long list of context entities and events they generate
|
||||
* And with regards to technological progress, we've changed nothing: now we have deprecated fields and events instead of deprecated methods.
|
||||
|
||||
|
@ -40,4 +40,4 @@ Do we want to say that REST is a meaningful concept? Definitely not. We only aim
|
||||
|
||||
On one hand, thanks to the multitude of interpretations, the API developers have built a perhaps vague but useful view of “proper” HTTP API architecture. On the other hand, the lack of concrete definitions has made REST API one of the most “holywar”-inspiring topics, and these holywars are usually quite meaningless as the popular REST concept has nothing to do with the REST described in Fielding's dissertation (and even more so, with the REST described in Fielding's manifesto of 2008).
|
||||
|
||||
The terms “REST architectural style” and its derivative “REST API” will not be used in the following chapters since it makes no practical sense as we explained above. We referred to the constraints described by Fielding many times in the previous chapters because, let us emphasize it once more, it is impossible to develop distributed client-server APIs without taking them into account. However, HTTP APIs (meaning JSON-over-HTTP endpoints utilizing the semantics described in the HTTP and URL standards) as we will describe them in the following chapter align well with the “average” understanding of “REST / RESTful API” as per numerous tutorials on the Web.
|
||||
The terms “REST architectural style” and its derivative “REST API” will not be used in the following chapters since it makes no practical sense as we explained above. We referred to the constraints described by Fielding many times in the previous chapters because, let us emphasize it once more, it is impossible to develop distributed client-server APIs without taking them into account. However, HTTP APIs (meaning JSON-over-HTTP endpoints utilizing the semantics described in the HTTP and URL standards) as we will describe them in the following chapter align well with the “average” understanding of “REST / RESTful API” as per numerous tutorials on the Web.
|
@ -94,18 +94,18 @@ HTTP verbs define two important characteristics of an HTTP call:
|
||||
|
||||
**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 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).
|
||||
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).
|
||||
|
||||
For obvious reasons, responses to modifying endpoints are not cached (though there are some conditions to use a response to a `POST` request as cached data for subsequent `GET` requests). This ensures that repeating `POST` / `PUT` / `DELETE` / `PATCH` requests will hit the server as no intermediary agent can respond with a cached result. In the case of a `GET` request, it is generally not true. Only the presence of `no-store` or `no-cache` directives in the response guarantees that the subsequent `GET` request will reach the server.
|
||||
For obvious reasons, responses to modifying endpoints are not cached (though there are some conditions to use a response to a `POST` request as cached data for subsequent `GET` requests). This ensures that repeating `POST` / `PUT` / `DELETE` / `PATCH` requests will hit the server as no intermediary agent can respond with a cached result. In the case of a `GET` request, it is generally not true. Only the presence of `no-store` or `no-cache` directives in the response guarantees that the subsequent `GET` request will reach the server.
|
||||
|
||||
One of the most widespread HTTP API design antipatterns is violating the semantics of HTTP verbs:
|
||||
* Placing modifying operations in a `GET` handler. This can lead to the following problems:
|
||||
* Interim agents might respond to such a request using a cached value if a required caching directive is missing, or vice versa, automatically repeat a request upon receiving a network timeout.
|
||||
* Some agents consider themselves eligible to traverse hyper-references (i.e., making HTTP `GET` requests) without the explicit user's consent. For example, social networks and messengers perform such calls to generate a preview for a link when a user tries to share it.
|
||||
* Placing non-idempotent operations in `PUT` / `DELETE` handlers. Although interim agents do not typically repeat modifying requests regardless of their alleged idempotency, a client or server framework can easily do so. This mistake is often coupled with requiring passing a body alongside a `DELETE` request to discern the specific object that needs to be deleted, which per se is a problem as any interim agent might discard such a body.
|
||||
* Ignoring the `GET` / `PUT` / `DELETE` symmetry requirement. This can manifest in different ways, such as:
|
||||
* Placing non-idempotent operations in `PUT` / `DELETE` handlers. Although interim agents do not typically repeat modifying requests regardless of their alleged idempotency, a client or server framework can easily do so. This mistake is often coupled with requiring passing a body alongside a `DELETE` request to discern the specific object that needs to be deleted, which per se is a problem as any interim agent might discard such a body.
|
||||
* Ignoring the `GET` / `PUT` / `DELETE` symmetry requirement. This can manifest in different ways, such as:
|
||||
* Making a `GET /url` operation return data even after a successful `DELETE /url` call
|
||||
* Making a `PUT /url` operation take the identifiers of the entities to modify from the request body instead of the URL, resulting in the `GET /url` operation's inability to return a representation of the entity passed to the `PUT /url` handler.
|
||||
|
||||
|
@ -19,11 +19,11 @@ the server could have simply responded with `400 Bad Request`, passing the reque
|
||||
|
||||
This situation (not only with JSON-RPC but with essentially every high-level protocol built on top of HTTP) has developed due to various reasons. Some of them are historical (such as the inability to use many HTTP protocol features in early implementations of the `XMLHttpRequest` functionality in web browsers). However, new RPC protocols relying on the bare minimum of HTTP capabilities continue to emerge today.
|
||||
|
||||
To answer this question, we must emphasize a very important distinction between application-level protocols (such as JSON-RPC in our case) and pure HTTP. A `400 BadRequest` is a transparent status for every intermediary network agent but a JSON-RPC custom error is not. Firstly, only a JSON-RPC-enabled client can read it. Secondly, and more importantly, in JSON-RPC, the request status *is not metadata*. In pure HTTP, the details of the operation, such as the method, requested URL, execution status, and request / response headers are readable *without the necessity to parse the entire body*. In most higher-level protocols, including JSON-RPC, this is not the case: even a protocol-enabled client must read a body to retrieve that information.
|
||||
To answer this question, we must emphasize a very important distinction between application-level protocols (such as JSON-RPC in our case) and pure HTTP. A `400 BadRequest` is a transparent status for every intermediary network agent but a JSON-RPC custom error is not. Firstly, only a JSON-RPC-enabled client can read it. Secondly, and more importantly, in JSON-RPC, the request status *is not metadata*. In pure HTTP, the details of the operation, such as the method, requested URL, execution status, and request / response headers are readable *without the necessity to parse the entire body*. In most higher-level protocols, including JSON-RPC, this is not the case: even a protocol-enabled client must read a body to retrieve that information.
|
||||
|
||||
How does an API developer benefit from the capability of reading request and response metadata? The modern client-server communication stack, as envisioned by Fielding, is multi-layered. We can enumerate a number of intermediary agents that process network requests and responses:
|
||||
* Frameworks that developers use to write code
|
||||
* Programming language APIs that frameworks are built on, and operating system APIs that compilers / interpreters of these languages rely on
|
||||
* Programming language APIs that frameworks are built on, and operating system APIs that compilers / interpreters of these languages rely on
|
||||
* Intermediary proxy servers between a client and a server
|
||||
* Various abstractions used in server programming, including server frameworks, programming languages, and operating systems
|
||||
* Web server software that is typically placed in front of backend handlers
|
||||
|
@ -9,7 +9,7 @@ Now let's discuss the specifics: what does it mean exactly to “follow the prot
|
||||
We need to apply these principles to an HTTP-based interface, adhering to the letter and spirit of the standard:
|
||||
* The URL of an operation must point to the resource the operation is applied to, serving as a cache key for `GET` operations and an idempotency key for `PUT` and `DELETE` operations.
|
||||
* HTTP verbs must be used according to their semantics.
|
||||
* Properties of the operation, such as safety, cacheability, idempotency, as well as the symmetry of `GET` / `PUT` / `DELETE` methods, request and response headers, response status codes, etc., must align with the specification.
|
||||
* Properties of the operation, such as safety, cacheability, idempotency, as well as the symmetry of `GET` / `PUT` / `DELETE` methods, request and response headers, response status codes, etc., must align with the specification.
|
||||
|
||||
**NB**: we're deliberately skipping many nuances of the standard:
|
||||
* A caching key might be composite (i.e., include request headers) if the response contains the `Vary` header.
|
||||
|
@ -50,7 +50,7 @@ Unfortunately, we don't have simple answers to these questions. Within this book
|
||||
3. 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.
|
||||
4. 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).
|
||||
5. 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.
|
||||
5. 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>`
|
||||
@ -75,7 +75,7 @@ One of the most popular tasks solved by exposing HTTP APIs is implementing CRUD
|
||||
**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.
|
||||
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.
|
||||
|
||||
@ -118,7 +118,7 @@ Let's start with the resource creation operation. As we remember from the “[Sy
|
||||
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:
|
||||
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>`.
|
||||
|
||||
|
@ -24,9 +24,9 @@ Additionally, we'd like to provide some code style advice:
|
||||
6. For every `GET` response, provide explicit caching parameters (otherwise, there is always a chance that a client or an intermediate agent invents them on their own).
|
||||
|
||||
7. Do not employ known possibilities to serve requests in violation of the standard and avoid exploiting “gray zones” of the protocol. In particular:
|
||||
* Do not place unsafe operations behind the `GET` verb, and do not place non-idempotent operations behind the `PUT` / `DELETE` methods.
|
||||
* Maintain the `GET` / `PUT` / `DELETE` operations symmetry.
|
||||
* Do not allow `GET` / `HEAD` / `DELETE` requests to have a body and do not provide bodies in response to `HEAD` requests or alongside the `204` status code.
|
||||
* Do not place unsafe operations behind the `GET` verb, and do not place non-idempotent operations behind the `PUT` / `DELETE` methods.
|
||||
* Maintain the `GET` / `PUT` / `DELETE` operations symmetry.
|
||||
* Do not allow `GET` / `HEAD` / `DELETE` requests to have a body and do not provide bodies in response to `HEAD` requests or alongside the `204` status code.
|
||||
* Do not invent your own standards for passing arrays and nested objects as query parameters. It is better to use an HTTP verb that allows having a body, or as a last resort pass the parameter as a base64-encoded JSON-stringified value.
|
||||
* Do not put parameters that require escaping (i.e., non-alphanumeric ones) in a path or a domain of a URL. Use query or body parameters for this purpose.
|
||||
|
||||
|
@ -8,7 +8,7 @@ The easiest and the most understandable case is that of providing a service for
|
||||
|
||||
There is also a plethora of monetizing techniques; in fact, we're just talking about monetizing software for developers.
|
||||
|
||||
1. The framework / library / platform might be paid per se, e.g., distributed under a commercial license. Nowadays such models are becoming less and less popular with the rise of free and open-source software but are still quite common.
|
||||
1. The framework / library / platform might be paid per se, e.g., distributed under a commercial license. Nowadays such models are becoming less and less popular with the rise of free and open-source software but are still quite common.
|
||||
|
||||
2. The API may be licensed under an open license with some restrictions that might be lifted by buying an extended license. It might be either functional limitations (an inability to publish the app in the app store or an incapacity to build the app in the production mode) or usage restrictions (for example, using the API for some purposes might be prohibited or an open license might be “contagious,” i.e., require publishing the derived code under the same license).
|
||||
|
||||
@ -42,7 +42,7 @@ B2B services are a special case. As B2B Service providers benefit from offering
|
||||
|
||||
**NB**: we rather disapprove the practice of providing an external API merely as a byproduct of the internal one without making any changes to bring value to the market. The main problem with such APIs is that partners' interests are not taken into account, which leads to numerous problems:
|
||||
* The API doesn't cover integration use cases well:
|
||||
* Internal customers employ quite a specific technological stack, and the API is poorly optimized to work with other programming languages / operating systems / frameworks.
|
||||
* Internal customers employ quite a specific technological stack, and the API is poorly optimized to work with other programming languages / operating systems / frameworks.
|
||||
* For external customers, the learning curve will be pretty flat as they can't take a look at the source code or talk to the API developers directly, unlike internal customers that are much more familiar with the API concepts.
|
||||
* Documentation often covers only some subset of use cases needed by internal customers.
|
||||
* The API services ecosystem which we will describe in “[The API Services Range](#api-product-range)” chapter usually doesn't exist.
|
||||
|
@ -21,7 +21,7 @@ However, frequently it makes sense to provide several API services manipulating
|
||||
2. The basic level of working with product entities via formal interfaces. [In our study example, that will be HTTP API for making orders.]
|
||||
3. Working with product entities might be simplified if SDKs are provided for some popular platforms that tailor API concepts according to the paradigms of those platforms (for those developers who are proficient with specific platforms only that will save a lot of effort on dealing with formal protocols and interfaces).
|
||||
4. The next simplification step is providing services for code generation. In this service, developers choose one of the pre-built integration templates, customize some options, and got a ready-to-use piece of code that might be simply copy-pasted into the application code (and might be additionally customized by adding some level 1-3 code). This approach is sometimes called “point-and-click programming.” [In the case of our coffee API, an example of such a service might have a form or screen editor for a developer to place UI elements and get the working application code.]
|
||||
5. Finally, this approach might be simplified even further if the service generates not code but a ready-to-use component / widget / frame and a one-liner to integrate it. [For example, if we allow embedding an iframe that handles the entire coffee ordering process right on the partner's website, or describes the rules of forming the image URL that will show the most relevant offer to an end user if embedded in the partner's app.]
|
||||
5. Finally, this approach might be simplified even further if the service generates not code but a ready-to-use component / widget / frame and a one-liner to integrate it. [For example, if we allow embedding an iframe that handles the entire coffee ordering process right on the partner's website, or describes the rules of forming the image URL that will show the most relevant offer to an end user if embedded in the partner's app.]
|
||||
|
||||
Ultimately, we will end up with a concept of meta-API, i.e., those high-level components will have an API of their own built on top of the basic API.
|
||||
|
||||
|
@ -10,8 +10,8 @@ However, sheer numbers might be deceiving, especially if we talk about free-to-u
|
||||
* The high-level API services that are meant for point-and-click integration (see the previous chapter) are significantly distorting the statistics, especially if the competitors don't provide such services; typically, for one full-scale integration there will be tens, maybe hundreds, of those lightweight embedded widgets.
|
||||
* Thereby, it's crucial to have partners counted for each kind of the integration independently.
|
||||
* Partners tend to use the API in suboptimal ways:
|
||||
* Embed it at every website page / application screen instead of only those where end users can really interact with the API
|
||||
* Put widgets somewhere deep in the page / screen footer, or hide it behind spoilers
|
||||
* Embed it at every website page / application screen instead of only those where end users can really interact with the API
|
||||
* Put widgets somewhere deep in the page / screen footer, or hide it behind spoilers
|
||||
* Initialize a broad range of API modules, but use only a limited subset of them.
|
||||
* The greater the API auditory is, the less the number of unique visitors means as at some moment the penetration will be close to 100%; for example, a regular Internet user interacts with Google or Facebook counters, well, every minute, so the daily audience of those API fundamentally cannot be increased further.
|
||||
|
||||
@ -35,7 +35,7 @@ This chapter would be incomplete if we didn't mention the “hygienic” KPI —
|
||||
|
||||
Still, let us re-iterate once more: any problems with your API are automatically multiplied by the number of partners you have, especially if the API is vital for them, i.e., the API outage makes the main functionality of their services unavailable. (And actually, because of the above-mentioned reasons, the average quality of integrations implies that partners' services will suffer even if the availability of the API is not formally speaking critical for them, but because developers use it excessively and do not bother with proper error handling.)
|
||||
|
||||
It is important to mention that predicting the workload for the API service is rather complicated. Sub-optimal API usage, e.g., initializing the API in those application and website parts where it's not actually needed, might lead to a colossal increase in the number of requests after changing a single line of partner's code. The safety margin for an API service must be much higher than for a regular service for end users — it must survive the situation of the largest partner suddenly starting querying the API on every page and every application screen. (If the partner is already doing that, then the API must survive doubling the load if the partner by accident starts initializing the API twice on each page / screen.)
|
||||
It is important to mention that predicting the workload for the API service is rather complicated. Sub-optimal API usage, e.g., initializing the API in those application and website parts where it's not actually needed, might lead to a colossal increase in the number of requests after changing a single line of partner's code. The safety margin for an API service must be much higher than for a regular service for end users — it must survive the situation of the largest partner suddenly starting querying the API on every page and every application screen. (If the partner is already doing that, then the API must survive doubling the load if the partner by accident starts initializing the API twice on each page / screen.)
|
||||
|
||||
Another extremely important hygienic minimum is the informational security of the API service. In the worst-case scenario, namely, if an API service vulnerability allows for exploiting partner applications, one security loophole will in fact be exposed *in every partner application*. Needless to say that the cost of such a mistake might be overwhelmingly colossal, even if the API itself is rather trivial and has no access to sensitive data (especially if we talk about webpages where no “sandbox” for third-party scripts exists, and any piece of code might let's say track the data entered in forms). API services must provide the maximum protection level (for example, choose cryptographical protocols with a certain overhead) and promptly react to any reports regarding possible vulnerabilities.
|
||||
|
||||
|
@ -44,7 +44,7 @@ This data is not itself reliable, but it allows for making cross-checks:
|
||||
* If a key was issued for one specific domain but requests are coming with a different `Referer`, it makes sense to investigate the situation and maybe ban the possibility to access the API with this `Referer` or this key.
|
||||
* If an application initializes API by providing a key registered to another application, it makes sense to contact the store administration and ask for removing one of the apps.
|
||||
|
||||
**NB**: don't forget to set infinite limits for using the API with the `localhost`, `127.0.0.1` / `[::1]` `Referer`s, and also for your own sandbox if it exists. Yes, abusers will sooner or later learn this fact and will start exploiting it, but otherwise, you will ban local development and your own website much sooner than that.
|
||||
**NB**: don't forget to set infinite limits for using the API with the `localhost`, `127.0.0.1` / `[::1]` `Referer`s, and also for your own sandbox if it exists. Yes, abusers will sooner or later learn this fact and will start exploiting it, but otherwise, you will ban local development and your own website much sooner than that.
|
||||
|
||||
The general conclusion is:
|
||||
* It is highly desirable to have partners formally identified (either through obtaining API keys or by providing contact data such as website domain or application identifier in a store while initializing the API).
|
||||
|
@ -29,9 +29,9 @@ Other popular mechanics of identifying robots include offering a bait (“honeyp
|
||||
|
||||
##### Restricting Access
|
||||
|
||||
The illusion of having a broad choice of technical means of identifying fraud users should not deceive you as you will soon discover the lack of effective methods of restricting those users. Banning them by cookie / `Referer` / `User-Agent` makes little to no impact as this data is supplied by clients, and might be easily forged. In the end, you have four mechanisms for suppressing illegal activities:
|
||||
The illusion of having a broad choice of technical means of identifying fraud users should not deceive you as you will soon discover the lack of effective methods of restricting those users. Banning them by cookie / `Referer` / `User-Agent` makes little to no impact as this data is supplied by clients, and might be easily forged. In the end, you have four mechanisms for suppressing illegal activities:
|
||||
* Banning users by IP (networks, autonomous systems)
|
||||
* Requiring mandatory user identification (maybe tiered: login / login with confirmed phone number / login with confirmed identity / login with confirmed identity and biometrics / etc.)
|
||||
* Requiring mandatory user identification (maybe tiered: login / login with confirmed phone number / login with confirmed identity / login with confirmed identity and biometrics / etc.)
|
||||
* Returning fake responses
|
||||
* Filing administrative abuse reports.
|
||||
|
||||
|
@ -17,7 +17,7 @@ Also, keep in mind that documentation will be used for searching as well, so eve
|
||||
|
||||
#### Documentation Content Types
|
||||
|
||||
##### Specification / Reference
|
||||
##### Specification / Reference
|
||||
|
||||
Any documentation starts with a formal functional description. This content type is the most inconvenient to use, but you must provide it. A reference is the hygienic minimum of the API documentation. If you don't have a doc that describes all methods, parameters, options, variable types, and their allowed values, then it's not an API but amateur dramatics.
|
||||
|
||||
@ -83,7 +83,7 @@ A significant problem that harms documentation clarity is API versioning: articl
|
||||
|
||||
If you're strictly maintaining backward compatibility, it is possible to create a single documentation for all API versions. To do so, each entity is to be marked with the API version it is supported from. However, there is an apparent problem with this approach: it's not that simple to get docs for a specific (outdated) API version (and, generally speaking, to understand which capabilities this API version provides). (Though the offline documentation we mentioned earlier will help.)
|
||||
|
||||
The problem becomes worse if you're supporting not only different API versions but also different environments / platforms / programming languages; for example, if your UI lib supports both iOS and Android. Then both documentation versions are equal, and it's impossible to pessimize one of them.
|
||||
The problem becomes worse if you're supporting not only different API versions but also different environments / platforms / programming languages; for example, if your UI lib supports both iOS and Android. Then both documentation versions are equal, and it's impossible to pessimize one of them.
|
||||
|
||||
In this case, you need to choose one of the following strategies:
|
||||
* If the documentation topic content is totally identical for every platform, i.e., only the code syntax differs, you will need to develop generalized documentation: each article provides code samples (and maybe some additional notes) for every supported platform on a single page.
|
||||
@ -95,4 +95,4 @@ The best documentation happens when you start viewing it as a product in the API
|
||||
|
||||
#### Was This Article Helpful to You?
|
||||
|
||||
[Yes / No](https://forms.gle/WPdQ9KsJt3fxqpyw6)
|
||||
[Yes / No](https://forms.gle/WPdQ9KsJt3fxqpyw6)
|
||||
|
@ -406,7 +406,7 @@ POST /v1/runtimes
|
||||
* обработчик `runs` в свою очередь выполнит операции своего уровня (в частности, проверит тип API кофемашины) и в зависимости от типа API пойдёт по одной из двух веток исполнения:
|
||||
* либо вызовет `GET /execution/status` физического API кофемашины, получит объём кофе и сличит с эталонным;
|
||||
* либо обратится к `GET /v1/runtimes/{runtime_id}`, получит `state.status` и преобразует его к статусу заказа;
|
||||
* в случае API второго типа цепочка продолжится: обработчик `GET /runtimes` обратится к физическому API `GET /sensors` и произведёт ряд манипуляций: сопоставит объём стакана / молотого кофе / налитой воды с запрошенным и при необходимости изменит состояние и статус.
|
||||
* в случае API второго типа цепочка продолжится: обработчик `GET /runtimes` обратится к физическому API `GET /sensors` и произведёт ряд манипуляций: сопоставит объём стакана / молотого кофе / налитой воды с запрошенным и при необходимости изменит состояние и статус.
|
||||
|
||||
**NB**: слова «цепочка вызовов» не следует воспринимать буквально. Каждый уровень может быть технически организован по-разному:
|
||||
* можно явно проксировать все вызовы по иерархии;
|
||||
@ -450,7 +450,7 @@ POST /v1/runtimes
|
||||
|
||||
Какие потоки данных мы имеем в нашем кофейном API?
|
||||
|
||||
1. Данные с сенсоров — объёмы кофе / воды / стакана. Это низший из доступных нам уровней данных, здесь мы не можем ничего изменить или переформулировать.
|
||||
1. Данные с сенсоров — объёмы кофе / воды / стакана. Это низший из доступных нам уровней данных, здесь мы не можем ничего изменить или переформулировать.
|
||||
|
||||
2. Непрерывный поток данных сенсоров мы преобразуем в дискретные статусы исполнения команд, вводя в него понятия, не существующие в предметной области. API кофемашины не предоставляет нам понятий «кофе наливается» или «стакан ставится» — это наше программное обеспечение трактует поступающие потоки данных от сенсоров, вводя новые понятия: если наблюдаемый объём (кофе или воды) меньше целевого — значит, процесс не закончен; если объём достигнут — значит, необходимо сменить статус исполнения и выполнить следующее действие.
|
||||
Важно отметить, что мы не просто вычисляем какие-то новые параметры из имеющихся данных сенсоров: мы сначала создаём новый кортеж данных более высокого уровня — «программа исполнения» как последовательность шагов и условий — и инициализируем его начальные значения. Без этого контекста определить, что собственно происходит с кофемашиной невозможно.
|
||||
|
@ -100,7 +100,7 @@ POST /v1/offers/search
|
||||
```
|
||||
|
||||
Здесь:
|
||||
* `offer` — некоторое «предложение»: на каких условиях можно заказать запрошенные виды кофе, если они были указаны, либо какое-то маркетинговое предложение — цены на самые популярные / интересные напитки, если пользователь не указал конкретные рецепты для поиска;
|
||||
* `offer` — некоторое «предложение»: на каких условиях можно заказать запрошенные виды кофе, если они были указаны, либо какое-то маркетинговое предложение — цены на самые популярные / интересные напитки, если пользователь не указал конкретные рецепты для поиска;
|
||||
* `place` — место (кафе, автомат, ресторан), где находится машина; мы не вводили эту сущность ранее, но, очевидно, пользователю потребуются какие-то более понятные ориентиры, нежели географические координаты, чтобы найти нужную кофемашину.
|
||||
|
||||
**NB**. Мы могли бы не добавлять новый эндпойнт, а обогатить существующий `/coffee-machines`. Однако такое решение выглядит менее семантично: не стоит в рамках одного интерфейса смешивать способ перечисления объектов по порядку и по релевантности запросу, поскольку эти два вида ранжирования обладают существенно разными свойствами и сценариями использования. К тому же, обогащение поиска «предложениями» скорее выводит эту функциональность из неймспейса «кофемашины»: для пользователя всё-таки первичен факт получения предложения приготовить напиток на конкретных условиях, и кофемашина — лишь одно из них, не самое важное.
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
Это соображение применимо ко всем принципам ниже. Если из-за следования правилам у вас получается неудобный, громоздкий, неочевидный API — это повод пересмотреть правила (или API).
|
||||
|
||||
Важно понимать, что вы вольны вводить свои собственные конвенции. Например, в некоторых фреймворках сознательно отказываются от парных методов `set_entity` / `get_entity` в пользу одного метода `entity` с опциональным параметром. Важно только проявить последовательность в её применении — если такая конвенция вводится, то абсолютно все методы API должны иметь подобную полиморфную сигнатуру, или по крайней мере должен существовать принцип именования, отличающий такие комбинированные методы от обычных вызовов.
|
||||
Важно понимать, что вы вольны вводить свои собственные конвенции. Например, в некоторых фреймворках сознательно отказываются от парных методов `set_entity` / `get_entity` в пользу одного метода `entity` с опциональным параметром. Важно только проявить последовательность в её применении — если такая конвенция вводится, то абсолютно все методы API должны иметь подобную полиморфную сигнатуру, или по крайней мере должен существовать принцип именования, отличающий такие комбинированные методы от обычных вызовов.
|
||||
|
||||
##### Явное лучше неявного
|
||||
|
||||
@ -161,10 +161,10 @@ GET /v1/coffee-machines/{id}⮠
|
||||
|
||||
##### Подобные сущности должны называться подобно и вести себя подобным образом
|
||||
|
||||
**Плохо**: `begin_transition` / `stop_transition`
|
||||
**Плохо**: `begin_transition` / `stop_transition`
|
||||
— `begin` и `stop` — непарные термины; разработчик будет вынужден рыться в документации.
|
||||
|
||||
**Хорошо**: `begin_transition` / `end_transition` либо `start_transition` / `stop_transition`.
|
||||
**Хорошо**: `begin_transition` / `end_transition` либо `start_transition` / `stop_transition`.
|
||||
|
||||
**Плохо**:
|
||||
```
|
||||
@ -330,7 +330,7 @@ POST /v1/users
|
||||
|
||||
Все эти проблемы должны решаться через введения ограничений на размеры полей и правильную декомпозицию эндпойнтов. Если в рамках одной сущности необходимо предоставлять как «лёгкие» (скажем, название и описание рецепта), так и «тяжёлые» данные (скажем, промо-фотография напитка, которая легко может по размеру превышать текстовые поля в сотни раз), лучше разделить эндпойнты и отдавать только ссылку для доступа к «тяжёлым» данным (в нашем случае, ссылку на изображение) — это, как минимум, позволит задавать различные политики кэширования для разных данных.
|
||||
|
||||
Неплохим упражнением здесь будет промоделировать типовой жизненный цикл основной функциональности приложения партнёра (например, выполнение одного заказа) и подсчитать общее количество запросов и объём трафика на один цикл. Причиной слишком большого числа запросов / объёма трафика может оказаться ошибка проектирования подсистемы уведомлений об изменениях состояния. Подробнее этот вопрос мы рассмотрим в главе [«Двунаправленные потоки данных»](#api-patterns-push-vs-poll) раздела «Паттерны API».
|
||||
Неплохим упражнением здесь будет промоделировать типовой жизненный цикл основной функциональности приложения партнёра (например, выполнение одного заказа) и подсчитать общее количество запросов и объём трафика на один цикл. Причиной слишком большого числа запросов / объёма трафика может оказаться ошибка проектирования подсистемы уведомлений об изменениях состояния. Подробнее этот вопрос мы рассмотрим в главе [«Двунаправленные потоки данных»](#api-patterns-push-vs-poll) раздела «Паттерны API».
|
||||
|
||||
##### Отсутствие результата — тоже результат
|
||||
|
||||
|
@ -58,7 +58,7 @@ const pendingOrders = await api.
|
||||
* идентификатор или идентификаторы последних модифицирующих операций, выполненных клиентом;
|
||||
* последняя известная клиенту версия ресурса (дата изменения, ETag).
|
||||
|
||||
Получив такой токен, сервер должен проверить, что ответ (список текущих операций, который он возвращает) соответствует токену, т.е. консистентность «в конечном счёте» сошлась. Если же она не сошлась (клиент передал дату модификации / версию / идентификатор последнего заказа новее, чем известна в данном узле сети), то сервер может реализовать одну из трёх стратегий (или их произвольную комбинацию):
|
||||
Получив такой токен, сервер должен проверить, что ответ (список текущих операций, который он возвращает) соответствует токену, т.е. консистентность «в конечном счёте» сошлась. Если же она не сошлась (клиент передал дату модификации / версию / идентификатор последнего заказа новее, чем известна в данном узле сети), то сервер может реализовать одну из трёх стратегий (или их произвольную комбинацию):
|
||||
|
||||
* запросить данные из нижележащего БД или другого хранилища повторно;
|
||||
* вернуть клиенту ошибку, индицирующую необходимость повторить запрос через некоторое время;
|
||||
@ -104,7 +104,7 @@ const pendingOrders = await api.
|
||||
|
||||
Математически вероятность получения ошибки выражается довольно просто: она равна отношению периода времени, требуемого для получения актуального состояния к типичному периоду времени, за который пользователь перезапускает приложение и повторяет заказ. (Следует, правда, отметить, что клиентское приложение может быть реализовано так, что даст вам ещё меньше времени, если оно пытается повторить несозданный заказ автоматически при запуске). Если первое зависит от технических характеристик системы (в частности, лага синхронизации, т.е. задержки репликации между мастером и копиями на чтение). А вот второе зависит от того, какого рода клиент выполняет операцию.
|
||||
|
||||
Если мы говорим о приложения для конечного пользователя, то типично время перезапуска измеряется для них в секундах, что в норме не должно превышать суммарного лага синхронизации — таким образом, клиентские ошибки будут возникать только в случае проблем с репликацией данных / ненадежной сети / перегрузки сервера.
|
||||
Если мы говорим о приложения для конечного пользователя, то типично время перезапуска измеряется для них в секундах, что в норме не должно превышать суммарного лага синхронизации — таким образом, клиентские ошибки будут возникать только в случае проблем с репликацией данных / ненадежной сети / перегрузки сервера.
|
||||
|
||||
Однако если мы говорим не о клиентских, а о серверных приложениях, здесь ситуация совершенно иная: если сервер решает повторить запрос (например, потому, что процесс был убит супервизором), он сделает это условно моментально — задержка может составлять миллисекунды. И в этом случае фон ошибок создания заказа будет достаточно значительным.
|
||||
|
||||
|
@ -294,7 +294,7 @@ POST /v1/partners/{id}/offers/history⮠
|
||||
|
||||
Небольшое примечание: признаком окончания перебора часто выступает отсутствие курсора на последней странице с данными; мы бы рекомендовали так не делать (т.е. всё же возвращать курсор, указывающий на пустой список), поскольку это позволит добавить функциональность динамической вставки данных в конец списка.
|
||||
|
||||
**NB**: в некоторых источниках перебор через идентификаторы / даты создания / курсор, напротив, не рекомендуется по следующей причине: пользователю невозможно показать список страниц и дать возможность выбрать произвольную. Здесь следует отметить, что:
|
||||
**NB**: в некоторых источниках перебор через идентификаторы / даты создания / курсор, напротив, не рекомендуется по следующей причине: пользователю невозможно показать список страниц и дать возможность выбрать произвольную. Здесь следует отметить, что:
|
||||
* подобный кейс — список страниц и выбор страниц — существует только для пользовательских интерфейсов; представить себе API, в котором действительно требуется доступ к случайным страницам данных мы можем с очень большим трудом;
|
||||
* если же мы всё-таки говорим об API приложения, которое содержит элемент управления с постраничной навигацией, то наиболее правильный подход — подготавливать данные для этого элемента управления на стороне сервера, в т.ч. генерировать ссылки на страницы;
|
||||
* подход с курсором не означает, что `limit`/`offset` использовать нельзя — ничто не мешает сделать двойной интерфейс, который будет отвечать и на запросы вида `GET /items?cursor=…`, и на запросы вида `GET /items?offset=…&limit=…`;
|
||||
|
@ -51,13 +51,13 @@ GET /v1/orders/created-history⮠
|
||||
|
||||
##### Сторонние сервисы отправки push-уведомлений
|
||||
|
||||
Одна из неприятных особенностей технологии типа long polling / WebSocket / SSE / MQTT — необходимость поддерживать открытое соединение между клиентом и сервером, что для мобильных приложений может быть проблемой с точки зрения производительности и энергопотребления. Один из вариантов решения этой проблемы — делегирование отправки уведомлений стороннему сервису (самым популярным выбором на сегодня является Firebase Cloud Messaging от Google), который в свою очередь доставит уведомление через встроенные механизмы платформы. Использование встроенных в платформу сервисов получения уведомлений снимает с разработчика головную боль по написанию кода, поддерживающего открытое соединение, и снижает риски неполучения сообщения. Недостатками third-party серверов сообщений является необходимость платить за них и ограничения на размер сообщения.
|
||||
Одна из неприятных особенностей технологии типа long polling / WebSocket / SSE / MQTT — необходимость поддерживать открытое соединение между клиентом и сервером, что для мобильных приложений может быть проблемой с точки зрения производительности и энергопотребления. Один из вариантов решения этой проблемы — делегирование отправки уведомлений стороннему сервису (самым популярным выбором на сегодня является Firebase Cloud Messaging от Google), который в свою очередь доставит уведомление через встроенные механизмы платформы. Использование встроенных в платформу сервисов получения уведомлений снимает с разработчика головную боль по написанию кода, поддерживающего открытое соединение, и снижает риски неполучения сообщения. Недостатками third-party серверов сообщений является необходимость платить за них и ограничения на размер сообщения.
|
||||
|
||||
Кроме того, отправка push-уведомлений на устройство конечного пользователя страдает от одной большой проблемы: процент успешной доставки уведомлений никогда не равен 100; потери сообщений могут достигать десятков процентов. С учётом ограничений на размер контента, скорее правильно говорить не о push-модели, а о комбинированной: приложение продолжает периодически опрашивать сервер, а пуши являются триггером для внеочередного опроса. (На самом деле, это соображение в той или иной мере применимо к любой технологии доставки сообщений на клиент. Низкоуровневые протоколы предоставляют больше возможностей управлять гарантиями доставки, но, с учётом ситуации с принудительным закрытием соединений системой, иметь в качестве страховки низкочастотный поллинг в приложении почти никогда не бывает лишним.)
|
||||
|
||||
#### Использование push-технологий в публичном API
|
||||
|
||||
Следствием описанной выше фрагментации клиентских технологий является фактическая невозможность использовать любую из них кроме обычного поллинга в публичном API. Требование к партнёрам реализовать получение сообщений через WebSocket / MQTT / SSE каналы значительно повышает порог входа в API, т.к. работа с низкоуровневыми протоколами, к тому же плохо покрытыми существующими IDL и кодогенерацией, требует значительных ресурсов и чревата ошибками имплементации. Если же вы решите предоставлять готовый SDK к такому API, то вам придётся самостоятельно разработать его под каждую целевую платформу (что, повторимся, само по себе трудоёмко). Учитывая, что HTTP-поллинг кратно проще в реализации, а его недостатки проявляются только там, где *действительно* нужно экономить трафик и вычислительные ресурсы, мы склонны рекомендовать предоставлять альтернативные каналы получения сообщений только *в дополнение* к поллингу, но никак не вместо него.
|
||||
Следствием описанной выше фрагментации клиентских технологий является фактическая невозможность использовать любую из них кроме обычного поллинга в публичном API. Требование к партнёрам реализовать получение сообщений через WebSocket / MQTT / SSE каналы значительно повышает порог входа в API, т.к. работа с низкоуровневыми протоколами, к тому же плохо покрытыми существующими IDL и кодогенерацией, требует значительных ресурсов и чревата ошибками имплементации. Если же вы решите предоставлять готовый SDK к такому API, то вам придётся самостоятельно разработать его под каждую целевую платформу (что, повторимся, само по себе трудоёмко). Учитывая, что HTTP-поллинг кратно проще в реализации, а его недостатки проявляются только там, где *действительно* нужно экономить трафик и вычислительные ресурсы, мы склонны рекомендовать предоставлять альтернативные каналы получения сообщений только *в дополнение* к поллингу, но никак не вместо него.
|
||||
|
||||
Хорошим решением для публичного API могли бы стать системные пуши, но здесь возникает другая проблема: разработчики приложений не склонны давать сторонним сервисам право на отсылку push-уведомлений, и на то есть большой список причин, начиная от расходов на отправку и заканчивая проблемами безопасности.
|
||||
|
||||
|
@ -105,7 +105,7 @@ registerProgramRunHandler(
|
||||
**NB**: в случае HTTP API соответствующий пример будет выглядеть более громоздко, поскольку потребует создания отдельных эндпойнтов для обмена сообщениями типа `GET /program-run/events` и `GET /partner/{id}/execution/events` — это упражнение мы оставляем читателю. Следует также отметить, что в реальных системах потоки событий часто направляют через внешнюю шину типа Apache Kafka или Amazon SNS/SQS.
|
||||
|
||||
Внимательный читатель может возразить нам, что фактически, если мы посмотрим на номенклатуру возникающих сущностей, мы ничего не изменили в постановке задачи, и даже усложнили её:
|
||||
* вместо вызова метода `takeout` мы теперь генерируем пару событий `takeout_requested` / `takeout_ready`;
|
||||
* вместо вызова метода `takeout` мы теперь генерируем пару событий `takeout_requested` / `takeout_ready`;
|
||||
* вместо длинного списка методов, которые необходимо реализовать для интеграции API партнёра, появляются длинные списки полей разных контекстов и событий, которые они генерирует;
|
||||
* проблема устаревания технологии не меняется, вместо устаревших методов мы теперь имеем устаревшие поля и события.
|
||||
|
||||
|
@ -102,18 +102,18 @@ HTTP-глагол определяет два важных свойства HTTP
|
||||
|
||||
**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`.
|
||||
Важное свойство модифицирующих идемпотентных глаголов — это то, что **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` запросов иметь тело (поскольку этому телу невозможно приписать никакой осмысленной роли). Однако (по-видимому в связи с тем, что многие разработчики попросту не знают семантику этих методов) распространённое ПО веб-серверов обычно разрешает этим методам иметь тело запроса и транслирует его дальше к коду обработки эндпойнта (использование этой практики мы решительно не рекомендуем).
|
||||
Идемпотентность и симметричность методов `GET` / `PUT` / `DELETE` влечёт за собой нежелательность для `GET` и `DELETE` запросов иметь тело (поскольку этому телу невозможно приписать никакой осмысленной роли). Однако (по-видимому в связи с тем, что многие разработчики попросту не знают семантику этих методов) распространённое ПО веб-серверов обычно разрешает этим методам иметь тело запроса и транслирует его дальше к коду обработки эндпойнта (использование этой практики мы решительно не рекомендуем).
|
||||
|
||||
Достаточно очевидным образом ответы на модифицирующие запросы не кэшируются (хотя при определённых условиях закэшированный ответ метода `POST` может быть использован при последующем `GET`-запросе) и, таким образом, повторный `POST` / `PUT` / `DELETE` / `PATCH` запрос обязательно будет доставлен до конечного сервера (ни один промежуточный агент не имеет права ответить из кэша). В случае `GET`-запроса это, вообще говоря, неверно — гарантией может служить только наличие в ответе директив кэширования `no-store` или `no-cache`.
|
||||
Достаточно очевидным образом ответы на модифицирующие запросы не кэшируются (хотя при определённых условиях закэшированный ответ метода `POST` может быть использован при последующем `GET`-запросе) и, таким образом, повторный `POST` / `PUT` / `DELETE` / `PATCH` запрос обязательно будет доставлен до конечного сервера (ни один промежуточный агент не имеет права ответить из кэша). В случае `GET`-запроса это, вообще говоря, неверно — гарантией может служить только наличие в ответе директив кэширования `no-store` или `no-cache`.
|
||||
|
||||
Один из самых частых антипаттернов разработки HTTP API — это использование HTTP-глаголов в нарушение их семантики:
|
||||
* Размещение модифицирующих операций за `GET`:
|
||||
* промежуточные агенты могут ответить на такой запрос из кэша, если какая-то из директив кэширования отсутствует, либо, напротив, повторить запрос при получении сетевого таймаута;
|
||||
* некоторые агенты считают себя вправе переходить по таким ссылкам без явного волеизъявления пользователя или разработчика; например, социальные сети и мессенджеры выполняют такие вызовы для генерации оформления ссылки, если пользователь пытается ей поделиться.
|
||||
* Размещение неидемпотентных операций за идемпотентными методами `PUT` / `DELETE`. Хотя промежуточные агенты редко автоматически повторяют модифицирующие запросы, тем не менее это легко может сделать используемый разработчиком клиента или сервера фреймворк. Обычно эта ошибка сочетается с наличием у `DELETE`-запроса тела (чтобы всё-таки отличать, что конкретно нужно перезаписать или удалить), что является само по себе проблемой, так как любой сетевой агент вправе это тело проигнорировать.
|
||||
* Несоблюдение требования симметричности операций `GET` / `PUT` / `DELETE`:
|
||||
* Размещение неидемпотентных операций за идемпотентными методами `PUT` / `DELETE`. Хотя промежуточные агенты редко автоматически повторяют модифицирующие запросы, тем не менее это легко может сделать используемый разработчиком клиента или сервера фреймворк. Обычно эта ошибка сочетается с наличием у `DELETE`-запроса тела (чтобы всё-таки отличать, что конкретно нужно перезаписать или удалить), что является само по себе проблемой, так как любой сетевой агент вправе это тело проигнорировать.
|
||||
* Несоблюдение требования симметричности операций `GET` / `PUT` / `DELETE`:
|
||||
* например, после выполнения `DELETE /url` операция `GET /url` продолжает возвращать какие-то данные или `PUT /url` ориентируется не на URL, а на данные внутри тела запроса для определения сущности, над которой выполняется операция, и, таким образом, `GET /url` никак не может вернуть представление объекта, только что переданного в `PUT /url`.
|
||||
|
||||
##### Статус-коды
|
||||
|
@ -9,7 +9,7 @@
|
||||
Эти принципы мы должны применить к протоколу HTTP, соблюдая дух и букву стандарта:
|
||||
* URL операции должен идентифицировать ресурс, к которому применяется действие, и быть ключом кэширования для `GET` и ключом идемпотентности — для `PUT` и `DELETE`;
|
||||
* HTTP-глаголы должны использоваться в соответствии с их семантикой;
|
||||
* свойства операции (безопасность, кэшируемость, идемпотентность, а также симметрия `GET` / `PUT` / `DELETE`-методов), заголовки запросов и ответов, статус-коды ответов должны соответствовать спецификации.
|
||||
* свойства операции (безопасность, кэшируемость, идемпотентность, а также симметрия `GET` / `PUT` / `DELETE`-методов), заголовки запросов и ответов, статус-коды ответов должны соответствовать спецификации.
|
||||
|
||||
**NB**: мы намеренно опускаем многие тонкости стандарта:
|
||||
* ключ кэширования фактически является составным [включает в себя заголовки запроса], если в ответе содержится заголовок `Vary`;
|
||||
|
@ -35,7 +35,7 @@
|
||||
|
||||
4. Насколько строго должна выдерживаться буквальная интерпретация конструкции `ГЛАГОЛ /ресурс`? Если мы принимаем правило «части URL обязаны быть существительными» (и ведь странно применять глагол к глаголу!), то в примерах выше должно быть не `prepare`, а `preparator` или `preparer` (а вариант `/action=prepare&coffee_machine_id=<id>&recipe=lungo` вовсе недопустим, так как нет объекта действия), что, честно говоря, лишь добавляет визуального шума в виде суффиксов «ator», но никак не способствует большей лаконичности и однозначности понимания.
|
||||
|
||||
5. Если сигнатура вызова по умолчанию модифицирующая или неидемпотентная, означает ли это, что операция *обязана* быть модифицирующей / идемпотентной? Двойственность смысловой нагрузки глаголов (семантика vs побочные действия) порождает неопределённость в вопросах организации API. Рассмотрим, например, ресурс `/v1/search`, осуществляющий поиск предложений кофе в нашем учебном API. С каким глаголом мы должны к нему обращаться?
|
||||
5. Если сигнатура вызова по умолчанию модифицирующая или неидемпотентная, означает ли это, что операция *обязана* быть модифицирующей / идемпотентной? Двойственность смысловой нагрузки глаголов (семантика vs побочные действия) порождает неопределённость в вопросах организации API. Рассмотрим, например, ресурс `/v1/search`, осуществляющий поиск предложений кофе в нашем учебном API. С каким глаголом мы должны к нему обращаться?
|
||||
* С одной стороны, `GET /v1/search?query=<поисковый запрос>` позволяет явно продекларировать, что никаких посторонних эффектов у этого запроса нет (никакие данные не перезаписываются) и результаты его можно кэшировать (при условии, что все значимые параметры передаются в URL).
|
||||
* С другой стороны, согласно семантике операции, `GET /v1/search` должен возвращать *представление ресурса `search`*. Но разве результаты поиска являются представлением ресурса-поисковика? Смысл операции «поиск» гораздо точнее описывается фразой «обработка запроса в соответствии с внутренней семантикой ресурса», т.е. соответствует методу `POST`. Кроме того, можем ли мы вообще говорить о кэшировании поисковых запросов? Страница результатов поиска формируется динамически из множества источников, и повторный запрос с той же поисковой фразой почти наверняка выдаст другой список результатов.
|
||||
|
||||
|
@ -43,7 +43,7 @@ If-Match: <ревизия>
|
||||
|
||||
Статус-коды, начинающиеся с цифры `4`, индицируют, что ошибка допущена пользователем или клиентом (или, по крайней мере, сервер так считает). *Обычно*, полученную `4xx` повторять бессмысленно — если не предпринять дополнительных действий по изменению состояния сервиса, этот запрос не будет выполнен успешно никогда. Однако из этого правила есть исключения, самые важные из которых — `429 Too Many Requests` и `404 Not Found`. Последняя по стандарту имеет смысл «состояния неопределённости»: сервер имеет право использовать её, если не желает раскрывать причины ошибки. После получения ошибки `404`, можно сделать повторный запрос, и он вполне может отработать успешно. Для индикации *персистентной* ошибки «ресурс не найден» используется отдельный статус `410 Gone`.
|
||||
|
||||
Более интересный вопрос — а что всё-таки клиент может (или должен) сделать, получив такую ошибку. Как мы указывали в главе «[Разграничение областей ответственности](#api-design-isolating-responsibility)», если ошибка может быть исправлена программно, необходимо в машиночитаемом виде индицировать это клиенту; если ошибка не может быть исправлена, необходимо включить человекочитаемые сообщения для пользователя (даже просто «попробуйте начать сначала / перезагрузить приложение» лучше с точки зрения UX, чем «неизвестная ошибка») и для разработчика, который будет разбираться с проблемой.
|
||||
Более интересный вопрос — а что всё-таки клиент может (или должен) сделать, получив такую ошибку. Как мы указывали в главе «[Разграничение областей ответственности](#api-design-isolating-responsibility)», если ошибка может быть исправлена программно, необходимо в машиночитаемом виде индицировать это клиенту; если ошибка не может быть исправлена, необходимо включить человекочитаемые сообщения для пользователя (даже просто «попробуйте начать сначала / перезагрузить приложение» лучше с точки зрения UX, чем «неизвестная ошибка») и для разработчика, который будет разбираться с проблемой.
|
||||
|
||||
С восстановимыми ошибками в HTTP, к сожалению, ситуация достаточно сложная. С одной стороны, протокол включает в себя множество специальных кодов, которые индицируют проблемы с использованием самого протокола — такие как `405 Method Not Allowed` (данный глагол неприменим к указанному ресурсу), `406 Not Acceptable` (сервер не может вернуть ответ согласно `Accept`-заголовкам запроса), `411 Length Required`, `414 URI Too Long` и так далее. Код клиента может обработать данные ошибки и даже, возможно, предпринять какие-то действия по их устранению (например, добавить заголовок `Content-Length` в запрос после получения ошибки `411`), но все они очень плохо применимы к ошибкам в бизнес-логике. Например, мы можем вернуть `429 Too Many Requests` при превышении лимитов запросов, но у нас нет никакого стандартного способа указать, *какой именно* лимит был превышен.
|
||||
|
||||
|
@ -24,9 +24,9 @@
|
||||
6. Для всех `GET`-запросов указывайте политику кэширования (иначе всегда есть шанс, что клиент или промежуточный агент придумает её за вас).
|
||||
|
||||
7. Не эксплуатируйте известные возможности оперировать запросами в нарушение стандарта и не изобретайте свои решения для «серых зон» протокола. В частности:
|
||||
* не размещайте модифицирующие операции за методом `GET` и неидемпотентные операции за `PUT` / `DELETE`;
|
||||
* соблюдайте симметрию `GET` / `PUT` / `DELETE` методов;
|
||||
* не позволяйте `GET` / `HEAD` / `DELETE`-запросам иметь тело, не возвращайте тело в ответе метода `HEAD` или совместно со статус-кодом `204 No Content`;
|
||||
* не размещайте модифицирующие операции за методом `GET` и неидемпотентные операции за `PUT` / `DELETE`;
|
||||
* соблюдайте симметрию `GET` / `PUT` / `DELETE` методов;
|
||||
* не позволяйте `GET` / `HEAD` / `DELETE`-запросам иметь тело, не возвращайте тело в ответе метода `HEAD` или совместно со статус-кодом `204 No Content`;
|
||||
* не придумывайте свой стандарт для передачи массивов и вложенных объектов в query — лучше воспользоваться HTTP-глаголом, позволяющим запросу иметь тело, или, в крайнем случае, передать параметры в виде base64-кодированного JSON-поля;
|
||||
* не размещайте в пути и домене URL параметры, по формату требующие эскейпинга (т.е. могущие содержать символы, отличные от цифр и букв латинского алфавита); для этой цели лучше воспользоваться query-параметрами или телом запроса.
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
Видов монетизации такого API существует множество — по сути, речь идёт о моделях монетизации ПО для разработчиков как таковом.
|
||||
|
||||
1. Фреймворк / библиотека / платформа могут быть платными сами по себе, т.е. распространяться под платной лицензией; в настоящее время такие модели становятся всё менее популярны в связи с растущим проникновением открытого программного обеспечения, но, тем не менее, всё ещё широко распространены.
|
||||
1. Фреймворк / библиотека / платформа могут быть платными сами по себе, т.е. распространяться под платной лицензией; в настоящее время такие модели становятся всё менее популярны в связи с растущим проникновением открытого программного обеспечения, но, тем не менее, всё ещё широко распространены.
|
||||
|
||||
2. API может быть лицензирован под открытой лицензией с определёнными ограничениями, которые могут быть сняты путём покупки расширенной лицензии; это может быть как ограничение функциональности API (например, запрет публикации приложения в соответствующем магазине приложений или невозможность сборки приложения в продакшен-режиме без приобретения лицензии), так и ограничения на использование (например, открытая лицензия может быть «заразной», т.е. требовать распространения написанного поверх платформы кода под той же лицензией, или же использование бесплатного API может быть запрещено для определённых целей).
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
|
||||
**NB**: мы всячески не рекомендуем при этом предоставление API «на сдачу», т.е. публикацию внутренних API без какой-либо дополнительной продуктовой и технической подготовки. Главная проблема таких API заключается в том, что интересы партнёров при планировании разработки никак не учитываются, что приводит к множественным проблемам.
|
||||
* API плохо покрывает основные сценарии интеграции:
|
||||
* внутренние потребители, как правило, используют вполне конкретный технический стек, и API плохо оптимизирован под любые другие языки программирования / операционные системы / фреймворки;
|
||||
* внутренние потребители, как правило, используют вполне конкретный технический стек, и API плохо оптимизирован под любые другие языки программирования / операционные системы / фреймворки;
|
||||
* внутренние потребители гораздо лучше погружены в контекст, могут посмотреть исходный код или пообщаться напрямую с разработчиком API, и для них порог входа в технологию находится на совершенно другом уровне по сравнению с внешними потребителями;
|
||||
* документация покрывает какой-то наиболее востребованный внутренними потребителями срез сценариев;
|
||||
* линейка сервисов API, о которой мы расскажем ниже, зачастую попросту отсутствует, т.к. внутренним потребителям она не нужна.
|
||||
|
@ -21,7 +21,7 @@
|
||||
2. Базовый уровень — работы с продуктовыми сущностями через формальные интерфейсы. [В случае нашего учебного API этому уровню соответствует HTTP API заказа.]
|
||||
3. Упростить работу с продуктовыми сущностями можно, предоставив SDK для различных платформ, скрывающие под собой сложности работы с формальными интерфейсами и адаптирующие концепции API под соответствующие парадигмы (что позволяет разработчикам, знакомым только с конкретной платформой, не тратить время и не разбираться в формальных интерфейсах и протоколах).
|
||||
4. Ещё более упростить работу можно с помощью сервисов, генерирующих код. В таком интерфейсе разработчик выбирает один из представленных шаблонов интеграции, кастомизирует некоторые параметры, и получает на выходе готовый фрагмент кода, который он может вставить в своё приложение (и, возможно, дописать необходимую функциональность с использованием API 1-3 уровней). Подобного рода подход ещё часто называют «программированием мышкой». [В случае нашего кофейного API примером такого сервиса мог бы служить визуальный редактор форм/экранов, в котором пользователь расставляет UI элементы, и в конечном итоге получает полный код приложения.]
|
||||
5. Ещё более упростить такой подход можно, если результатом работы такого сервиса будет уже не код поверх API, а готовый компонент / виджет / фрейм, подключаемый одной строкой. [Например, если мы дадим возможность разработчику вставлять на свой сайт iframe, в котором можно заказать кофе, кастомизированный под нужды заказчика, либо, ещё проще, описать правила формирования URL изображения, который позволит показать в приложении партнёра баннер с наиболее релевантным предложением для конечного пользователя.]
|
||||
5. Ещё более упростить такой подход можно, если результатом работы такого сервиса будет уже не код поверх API, а готовый компонент / виджет / фрейм, подключаемый одной строкой. [Например, если мы дадим возможность разработчику вставлять на свой сайт iframe, в котором можно заказать кофе, кастомизированный под нужды заказчика, либо, ещё проще, описать правила формирования URL изображения, который позволит показать в приложении партнёра баннер с наиболее релевантным предложением для конечного пользователя.]
|
||||
|
||||
В конечном итоге можно прийти к концепции мета-API, когда готовые визуальные компоненты тоже будут иметь какое-то свой высокоуровневый API, который «под капотом» будет обращаться к базовым API.
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
* партнёры склонны использовать API неоптимально:
|
||||
|
||||
* подключать его на всех страницах / экранах приложения, а не только там, где пользователь реально с ним взаимодействует;
|
||||
* подключать его на всех страницах / экранах приложения, а не только там, где пользователь реально с ним взаимодействует;
|
||||
* размещать виджеты глубоко в «подвале» или за спойлером;
|
||||
* инициализировать широкий набор модулей, но использовать только тривиальную функциональность;
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
|
||||
Тем не менее, позволим себе ещё раз напомнить: любые проблемы вашего API автоматически умножаются на количество партнёров, особенно в тех случаях, когда ваш API критически для них важен, т.е. при неработоспособности API становится недоступной основная функциональность сервиса. (Впрочем, по упомянутым выше причинам качество интеграции бо́льшей части партнёров почти неизбежно будет таково, что ошибки в работе их сервисов будут происходить, даже если ваш API не является формально для них критическим — а потому, что разработчики подключают API даже там, где оно формально не нужно, и пренебрегают обработкой ошибок.)
|
||||
|
||||
Важно отметить, что нагрузку на API, вообще говоря, крайне сложно предсказать. Неоптимальное использование API, т.е. его инициализация в тех разделах приложений, где он в реальности не нужен, может привести к колоссальному росту нагрузки вследствие перемещения одной-единственной строки кода партнёра. «Запас прочности» API-сервис должен быть гораздо выше, чем у обычных пользовательских сервисов — как минимум на уровне, достаточном для поддержания его работоспособности в том случае, если крупнейший партнёр начнёт вызывать API при загрузке любой страницы вебсайта / открытии любого экрана приложения. (Если партнёр уже так делает — то API должен переживать как минимум удвоение этой нагрузки на тот случай, если разработчики случайно начнут инициализировать API дважды.)
|
||||
Важно отметить, что нагрузку на API, вообще говоря, крайне сложно предсказать. Неоптимальное использование API, т.е. его инициализация в тех разделах приложений, где он в реальности не нужен, может привести к колоссальному росту нагрузки вследствие перемещения одной-единственной строки кода партнёра. «Запас прочности» API-сервис должен быть гораздо выше, чем у обычных пользовательских сервисов — как минимум на уровне, достаточном для поддержания его работоспособности в том случае, если крупнейший партнёр начнёт вызывать API при загрузке любой страницы вебсайта / открытии любого экрана приложения. (Если партнёр уже так делает — то API должен переживать как минимум удвоение этой нагрузки на тот случай, если разработчики случайно начнут инициализировать API дважды.)
|
||||
|
||||
Другой важнейший гигиенический минимум — это обеспечение информационной безопасности API. В худшем из возможных сценариев, если посредством эксплуатации уязвимости в API можно будет наносить вред конечным пользователем, фактически дыра в безопасности будет создана *в каждом приложении партнёра*. Излишне уточнять, что цена такой ошибки может оказаться невероятно большой даже если само API достаточно невинно и никакого доступа к чувствительным данным не имеет (особенно если речь идёт о веб-страницах, где в принципе нет никакой «песочницы» для сторонних скриптов, и любой код на странице может, например, отследить вводимые данные в формы). Сервисы API обязаны как использовать максимальные меры защиты (например, с запасом выбирать надёжные криптографические протоколы), так и максимально оперативно реагировать на сообщения об уязвимостях.
|
||||
|
||||
|
@ -47,7 +47,7 @@
|
||||
* если ключ был выпущен для одного домена, но запросы приходят с `Referer`-ом другого домена — это повод разобраться в ситуации и, возможно, забанить возможность обращаться к API с этим `Referer`-ом или этим ключом;
|
||||
* если одно приложение инициализирует API с указанием ключа другого приложения — это повод обратиться к администрации стора с требованием удалить одно из приложений.
|
||||
|
||||
**NB**: не забудьте разрешить безлимитное использование с `Referer`-ом `localhost` и `127.0.0.1` / `[::1]`, а также из вашей собственной песочницы, если она есть. Да, в какой-то момент злоумышленники поймут, что на такие `Referer`-ы не действуют ограничения, но это точно произойдёт гораздо позже, чем вы по неосторожности забаните локальную разработку или собственный сайт документации.
|
||||
**NB**: не забудьте разрешить безлимитное использование с `Referer`-ом `localhost` и `127.0.0.1` / `[::1]`, а также из вашей собственной песочницы, если она есть. Да, в какой-то момент злоумышленники поймут, что на такие `Referer`-ы не действуют ограничения, но это точно произойдёт гораздо позже, чем вы по неосторожности забаните локальную разработку или собственный сайт документации.
|
||||
|
||||
Общий вывод из вышеизложенного таков:
|
||||
* очень желательно иметь формальную идентификацию пользователей (API-ключи как самая распространённая практика, либо указание контактных данных, таких как домен вебсайта или идентификатор приложения в сторе, при инициализации API);
|
||||
|
@ -29,9 +29,9 @@
|
||||
|
||||
##### Ограничение доступа
|
||||
|
||||
Видимость богатства способов технической идентификации пользователей, увы, разбивается о суровую реальность наличия у вас очень скромных средств ограничения доступа. Бан по cookie / `Referer`-у / `User-Agent`-у практически не работает по той причине, что эти данные передаёт клиент, и он же легко может их подменить. По большому счёту, способов ограничения доступа у вас четыре:
|
||||
Видимость богатства способов технической идентификации пользователей, увы, разбивается о суровую реальность наличия у вас очень скромных средств ограничения доступа. Бан по cookie / `Referer`-у / `User-Agent`-у практически не работает по той причине, что эти данные передаёт клиент, и он же легко может их подменить. По большому счёту, способов ограничения доступа у вас четыре:
|
||||
* бан пользователя по ip (подсети, автономной системе);
|
||||
* требование обязательной идентификации пользователя (возможно, прогрессивной: логин в системе / логин с подтверждённым номером телефона / логин с подтверждением личности / логин с подтверждением личности и биометрией);
|
||||
* требование обязательной идентификации пользователя (возможно, прогрессивной: логин в системе / логин с подтверждённым номером телефона / логин с подтверждением личности / логин с подтверждением личности и биометрией);
|
||||
* отдача ложного ответа;
|
||||
* борьба административными методами.
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
#### Виды справочных материалов
|
||||
|
||||
##### Спецификация / справочник / референс
|
||||
##### Спецификация / справочник / референс
|
||||
|
||||
Любая документация начинается с формального описания доступной функциональности. Да, этот вид документации будет максимально бесполезен с точки зрения удобства использования, но не предоставлять его нельзя — справочник является гигиеническим минимумом. Если у вас нет документа, в котором описаны все методы, параметры и настройки, типы всех переменных и их допустимые значения, зафиксированы все опции и поведения — это не API, а просто какая-то самодеятельность.
|
||||
|
||||
@ -85,7 +85,7 @@ Quick start-ы также являются отличным индикаторо
|
||||
|
||||
Если вы поддерживаете обратную совместимость, то можно попытаться поддерживать единую документацию для всех версий API. В этом случае для каждой сущности нужно указывать, начиная с какой версии API появилась её поддержка. Здесь, однако, возникает проблема с тем, что получить документацию для какой-то конкретной (устаревшей) версии API (и вообще понять, какие возможности предоставляла определённая версия API) крайне затруднительно. (Но с этим может помочь офлайн-документация, о чём мы упоминали выше.)
|
||||
|
||||
Проблема осложняется, если вы поддерживаете документацию не только для разных версий API, но и для разных сред / платформ / языков программирования — скажем, ваша визуальная библиотека поддерживает и Android, и iOS. В этой ситуации обе версии документации полностью равноправны, и выделить одну из них в ущерб другой невозможно.
|
||||
Проблема осложняется, если вы поддерживаете документацию не только для разных версий API, но и для разных сред / платформ / языков программирования — скажем, ваша визуальная библиотека поддерживает и Android, и iOS. В этой ситуации обе версии документации полностью равноправны, и выделить одну из них в ущерб другой невозможно.
|
||||
|
||||
В этом случае необходимо выбрать одну из двух стратегий:
|
||||
* если контент справки полностью идентичен для всех платформ, т.е. меняется только синтаксис кода — придётся подготовить возможность писать документацию обобщённым образом: статьи документации должны содержать примеры кода (и, возможно, какие-то примечания) сразу для всех поддерживаемых платформ;
|
||||
@ -97,4 +97,4 @@ Quick start-ы также являются отличным индикаторо
|
||||
|
||||
#### Была ли эта статья полезна для вас?
|
||||
|
||||
[Да / Нет](https://forms.gle/nRB7PRgLSieeC7Je8)
|
||||
[Да / Нет](https://forms.gle/nRB7PRgLSieeC7Je8)
|
||||
|
Loading…
x
Reference in New Issue
Block a user