1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-05-25 22:08:06 +02:00

Chapter 11 extension translated

This commit is contained in:
Sergey Konstantinov 2021-01-03 21:11:03 +03:00
parent 9f97cb4838
commit c63257eac3
5 changed files with 555 additions and 84 deletions

View File

@ -22,14 +22,6 @@ See full license in LICENSE.md file or at [Creative Commons Website](http://crea
Right now Section I (‘API Design’) is finished. The Section is lacking readable schemes, I'll draw them later.
TODO for Section I:
* double negations;
* eventual consistency;
* truthy default values;
* partial updates;
* empty arrays;
* errors order.
The book will contain two more sections.
* Section II ‘Backwards Compatibility’ will cover growth issues. Major themes are:
* major sources of problems leading to backwards compatibility breach;
@ -54,7 +46,7 @@ I also have more distant plans on adding two more subsections to Section I.
## Translation
I will translate sections into English at the moment they're ready. Which means that Section I will be translated soon.
I am translating new chapters into English at the moment they're ready. I'm not a native speaker, so feel free to correct my grammar.
## Contributing

View File

@ -14,7 +14,7 @@ This idea applies to every concept listed below. If you get an unusable, bulky,
It is important to understand that you always can introduce the concepts of your own. For example, some frameworks willfully reject paired `set_entity` / `get_entity` methods in a favor of a single `entity()` method, with an optional argument. The crucial part is being systematic in applying the concept. If it's rendered into life, you must apply it to every single API method, or at the very least elaborate a naming rule to discern such polymorphic methods from regular ones.
##### 1. Explicit is always better than implicit
##### Explicit is always better than implicit
Entity's name must explicitly tell what it does and what side effects to expect while using it.
@ -55,7 +55,7 @@ Two important implications:
**1.2.** If your API's nomenclature contains both synchronous and asynchronous operations, then (a)synchronicity must be apparent from signatures, **or** a naming convention must exist.
##### 2. Specify which standards are used
##### Specify which standards are used
Regretfully, the humanity is unable to agree on the most trivial things, like which day starts the week, to say nothing about more sophisticated standards.
@ -76,15 +76,15 @@ So *always* specify exactly which standard is applied. Exceptions are possible,
One particular implication from this rule is that money sums must *always* be accompanied with a currency code.
It is also worth saying that in some areas the situation with standards is so spoiled that, whatever you do, someone got upset. A ‘classical’ example is geographical coordinates order (latitude-longitude vs longitude-latitude). Alas, the only working method of fighting with frustration there is a ‘serenity notepad’ to be discussed in Section II.
It is also worth saying that in some areas the situation with standards is so spoiled that, whatever you do, someone got upset. A ‘classical’ example is geographical coordinates order (latitude-longitude vs longitude-latitude). Alas, the only working method of fighting with frustration there is the ‘Serenity Notepad’ to be discussed in Section II.
##### 3. Keep fractional numbers precision intact
##### Keep fractional numbers precision intact
If the protocol allows, fractional numbers with fixed precision (like money sums) must be represented as a specially designed type like Decimal or its equivalent.
If there is no Decimal type in the protocol (for instance, JSON doesn't have one), you should either use integers (e.g. apply a fixed multiplicator) or strings.
##### 4. Entities must have concrete names
##### Entities must have concrete names
Avoid single amoeba-like words, such as get, apply, make.
@ -92,7 +92,7 @@ Avoid single amoeba-like words, such as get, apply, make.
**Better**: `user.get_id()`.
##### 5. Don't spare the letters
##### Don't spare the letters
In XXI century there's no need to shorten entities' names.
@ -113,7 +113,7 @@ Possibly, an author of this API thought that `pbrk` abbreviature would mean some
**Better**: `str_search_for_characters (lookup_character_set, str)`
— though it's highly disputable whether this function should exist at all; a feature-rich search function would be much more convenient. Also, shortening `string` to `str` bears no practical sense, regretfully being a routine in many subject areas.
##### 6. Naming implies typing
##### Naming implies typing
Field named `recipe` must be of `Recipe` type. Field named `recipe_id` must contain a recipe identifier which we could find within `Recipe` entity.
@ -143,7 +143,7 @@ Word ‘function’ is many-valued. It could mean builtin functions, but also
**Better**: `GET /v1/coffee-machines/{id}/builtin-functions-list`
##### 7. Matching entities must have matching names and behave alike
##### Matching entities must have matching names and behave alike
**Bad**: `begin_transition` / `stop_transition`
`begin` and `stop` doesn't match; developers will have to dig into the docs.
@ -168,56 +168,238 @@ Several rules are violated:
We're leaving the exercise of making these signatures better to the reader.
##### 8. Clients must always know full system state
##### Use globally unique identifiers
It's considered good form to use globally unique strings as entity identifiers, either semantic (i.e. "lungo" for beverage types) or random ones (i.e. [UUID-4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)). It might turn out extremely useful should you merge data from several sources under single identifier.
In general, we tend to advice using urn-like identifiers, e.g. `urn:order:<uuid>` (or just `order:<uuid>`). That helps a lot when dealing with legacy systems with different identifiers attached to the same entity. Namespaces in urns help to understand quickly which identifier is used, and is there a usage mistake.
One important implication: **never use increasing numbers as external identifiers**. Apart from abovementioned reasons, it allows counting how many entities of each types there are in the system. You competitors will be able to calculate a precise number of orders you have each day, for example.
**NB**: this book often use short identifiers like "123" in code examples; that's for reading the book on small screens convenience, do not replicate this practice in a real-world API.
##### Clients must always know full system state
This rule could be reformulated as ‘don't make clients guess’.
**Bad**:
```
// Creates a comment and returns its id
POST /comments
{ "content" }
// Creates an order and returns its id
POST /v1/orders
{ }
{ "comment_id" }
{ "order_id" }
```
```
// Returns a comment by its id
GET /comments/{id}
{
// The comment isn't published
// until the captcha is solved
"published": false,
"action_required": "solve_captcha",
"content"
}
// Returns an order by its id
GET /v1/orders/{id}
// The order isn't confirmed
// and awaits checking
→ 404 Not Found
```
— though the operation pretends to be successful, clients must perform an additional action to understand the comment's real state. In between `POST /comments` and `GET /comments/{id}` calls client remains in ‘Schrödinger's cat’ state: it is unknown whether the comment is published or not, and how to display this state to a user.
— though the operation looks to be executed successfully, the client must store order id and recurrently check `GET /v1/orders/{id}` state. This pattern is bad per se, but gets even worse when we consider two cases:
* clients might lose the id, if system failure happened in between sending the request and getting the response, or if app data storage was damaged or cleansed;
* customers can't use another device; in fact, the knowledge of orders being created is bound to a specific user agent.
In both cases customers might consider order creating failed, and make a duplicate order, with all the consequences to be blamed on you.
**Better**:
```
// Creates a comment and returns it
POST /v1/comments
{ "content" }
// Creates an order and returns it
POST /v1/orders
{ <order parameters> }
{ "comment_id", "published", "action_required", "content" }
```
```
// Returns a comment by its id
GET /v1/comments/{id}
{ /* exactly the same format,
as in `POST /comments` reponse */
{
"order_id",
// The order is created in explicit
// «checking» status
"status": "checking",
}
```
Generally speaking, in 9 cases out of 10 it is better to return a full entity state from any modifying operation, sharing the format with read access endpoint. Actually, you should *always* do this unless response size affects performance.
```
// Returns an order by its id
GET /v1/orders/{id}
{ "order_id", "status" … }
```
Same observation applies to filling default values either. Don't make client guess what default values are, or, even worse, hardcode them — return the values of all non-required fields in creation / rewriting endpoints response.
##### Avoid double negations
##### 9. Idempotency
**Bad**: `"dont_call_me": false`
— people are bad at perceiving double negation, making mistakes.
All API operations must be idempotent. Let us recall that idempotency is the following property: repeated calls to the same function with the same parameters don't change the resource state. Since we're discussing client-server interaction in a first place, repeating request in case of network failure isn't an exception, but a norm of life.
**Better**: `"prohibit_calling": true` or `"avoid_calling": true`
— it's easier to read, though you shouldn't deceive yourself. Avoid semantical double negations, even if you've found a ‘negative’ word without ‘negative’ prefix.
Also worth mentioning, that making mistakes in [de Morgan's laws](https://en.wikipedia.org/wiki/De_Morgan's_laws) usage is even simpler. For example, if you have two flags:
```
GET /coffee-machines/{id}/stocks
{
"has_beans": true,
"has_cup": true
}
```
‘Coffee might be prepared’ condition would look like `has_beans && has_cup` — both flags must be true. However, if you provide the negations of both flags:
```
{
"beans_absence": false,
"cup_absence": false
}
```
— then developers will have to evaluate one of `!beans_absence && !cup_absence``!(beans_absence || cup_absence)` conditions, and in this transition people tend to make mistakes. Avoiding double negations helps little, and regretfully only general advice could be given: avoid the situations, when developers have to evaluate such flags.
##### Avoid implicit type conversion
This advice is opposite to the previous one, ironically. When developing APIs you frequently need to add new optional field with non-empty default value. For example:
```
POST /v1/orders
{}
{
"contactless_delivery": true
}
```
New `contactless_delivery` options isn't required, but its default value is `true`. A question arises: how developers should discern explicit intention to abolish the option (`false`) from knowing not it exists (field isn't set). They have to write something like:
```
if (Type(order.contactless_delivery) == 'Boolean' &&
order.contactless_delivery == false) { … }
```
This practice makes the code more complicated, and it's quite easy to make mistakes, which will effectively treat the field in a quite opposite manner. Same could happen if some special values (i.e. `null` or `-1`) to denote value absence are used.
The universal rule to deal with such situations is to make all new Boolean flags being false by default. If a non-Boolean field with specially treated value absence is to be introduced, then introduce two fields.
**Bad**:
```
// Creates a user
POST /users
{ … }
// Users are created with a monthly
// spendings limit set by default
{
"spendings_monthly_limit_usd": "100"
}
// To cancel the limit null value is used
POST /users
{
"spendings_monthly_limit_usd": null
}
```
**Better**
```
POST /users
{
// true — user explicitly cancels
// monthly spendings limit
// false — limit isn't canceled
// (default value)
"abolish_spendings_limit": false,
// Non-required field
// Only present if the previous flag
// is set to false
"spendings_monthly_limit_usd": "100",
}
```
**NB**: the contradiction with the previous rule lies in the necessity of introducing ‘negative’ flags (the ‘no limit’ flag), which we had to rename to `abolish_spendings_limit`. Though it's a decent name for a negative flag, its semantics is still unobvious, and developers will have to read the docs. That's the way.
##### Avoid partial updates
**Bad**:
```
// Return the order state
// by its id
GET /v1/orders/123
{
"order_id",
"delivery_address",
"client_phone_number",
"client_phone_number_ext",
"updated_at"
}
// Partially rewrites the order
PATCH /v1/orders/123
{ "delivery_address" }
{ "delivery_address" }
```
— this approach is usually taken to lessen request and response body size, plus it allows to implement collaborative editing cheaply. Both these advantages are imaginary.
In first, sparing bytes is seldom needed in modern apps. Network packets sizes (MTU, Maximum Transmission Unit) are more than a kilobyte right now; shortening responses is useless while they're less then a kilobyte. More viable approach would be separate endpoints to deal with large chunks of data and left all other endpoints data-rich.
In seconds, shortening response sizes will backfire exactly with implementing collaborative editing: one client won't see the changes the other client have made. Generally speaking, in 9 cases out of 10 it is better to return a full entity state from any modifying operation, sharing the format with read access endpoint. Actually, you should always do this unless response size affects performance.
In third, this approach might work if you need to rewrite a field's value. But how to unset the field, return its value to the default state? For example, how to *remove* `client_phone_number_ext`?
In such cases special values are often being used, like `null`. But as we discussed above, this is a defective practice. Another variant is prohibiting non-required fields, but that would pose considerable obstacles in a way of expanding the API.
**Better**: one of the following two strategies might be used.
**Option \#1**: splitting the endpoints. Editable fields are grouped and taken out as separate endpoints. This approach also matches well against [the decomposition principle](#chapter-10) we discussed in the previous chapter.
```
// Return the order state
// by its id
GET /v1/orders/123
{
"order_id",
"delivery_details": {
"address"
},
"client_details": {
"phone_number",
"phone_number_ext"
},
"updated_at"
}
// Fully rewrite order delivery options
PUT /v1/orders/123/delivery-details
{ "address" }
// Fully rewrite order customer data
PUT /v1/orders/123/client-details
{ "phone_number" }
```
Omitting `client_phone_number_ext` in `PUT client-details` request would be sufficient to remove it. This approach also helps to separate constant and calculated fields (`order_id` and `updated_at`) from editable ones, thus getting rid of ambiguous situations (what happens id a client tries to rewrite the `updated_at` field?). You may also return the entire `order` entity from `PUT` endpoints (however, there should be some naming convention for that).
**Option 2**: design a format for atomic changes.
```
POST /v1/order/changes
X-Idempotency-Token: <see next paragraph>
{
"changes": [{
"type": "set",
"field": "delivery_address",
"value": <new value>
}, {
"type": "unset",
"field": "client_phone_number_ext"
}]
}
```
This approach is much harder to implement, but it's the only viable method to implement collaborative editing, since it's explicitly reflects what a user was actually doing with entity representation. With data exposed in such a format you might actually implement offline editing, when user changes are accumulated and then sent at once, while the server automatically resolves conflicts by ‘rebasing’ the changes.
##### All API operations must be idempotent
Let us recall that idempotency is the following property: repeated calls to the same function with the same parameters don't change the resource state. Since we're discussing client-server interaction in a first place, repeating request in case of network failure isn't an exception, but a norm of life.
If endpoint's idempotency can't be assured naturally, explicit idempotency parameters must be added, in a form of either a token or a resource version.
@ -293,13 +475,164 @@ X-Idempotency-Token: <token>
```
— the server found out that a different token was used in creating revision 124, which means an access conflict.
Furthermore, adding idempotency tokens not only resolves the issue, but also makes possible to make an advanced optimization. If the server detects an access conflict, it could try to resolve it, ‘rebasing’ the update like modern version control systems do, and return `200 OK` instead of `409 Conflict`. This logics dramatically improves user experience, being fully backwards compatible (providing your API embraces the rule \#9) and avoiding conflict resolving code fragmentation.
Furthermore, adding idempotency tokens not only resolves the issue, but also makes possible to make an advanced optimization. If the server detects an access conflict, it could try to resolve it, ‘rebasing’ the update like modern version control systems do, and return `200 OK` instead of `409 Conflict`. This logics dramatically improves user experience, being fully backwards compatible and avoiding conflict resolving code fragmentation.
Also, be warned: clients are bad at implementing idempotency tokens. Two problems are common:
* you can't really expect that clients generate truly random tokens — they may share the same seed or simply use weak algorithms or entropy sources; therefore you must put constraints on token checking: token must be unique to specific user and resource, not globally;
* clients tend to misunderstand the concept and either generate new tokens each time they repeat the request (which deteriorates the UX, but otherwise healthy) or conversely use one token in several requests (not healthy at all and could lead to catastrophic disasters; another reason to implement the suggestion in the previous clause); writing detailed doc and/or client library is highly recommended.
##### 10. Caching
##### Avoid non-atomic operations
There is a common problem with implementing the changes list approach: what to do, if some changes were successfully applied, while others are not? The rule is simple: if you may ensure the atomicity (e.g. either apply all changes or none of them) — do it.
**Bad**:
```
// Returns a list of recipes
GET /v1/recipes
{
"recipes": [{
"id": "lungo",
"volume": "200ml"
}, {
"id": "latte",
"volume": "300ml"
}]
}
// Changes recipes' parameters
PATCH /v1/recipes
{
"changes": [{
"id": "lungo",
"volume": "300ml"
}, {
"id": "latte",
"volume": "-1ml"
}]
}
→ 400 Bad Request
// Re-reading the list
GET /v1/recipes
{
"recipes": [{
"id": "lungo",
// This value changed
"volume": "300ml"
}, {
"id": "latte",
// and this did not
"volume": "300ml"
}]
}
```
— there is no way how client might learn that failed operation was actually partially applied. Even if there is an indication of this fact in the answer, the client still cannot tell, whether lungo volume changed because of the request, or some other client changed it.
If you can't guarantee the atomicity of an operation, you should elaborate in details how to deal with it. There must be a separate status for each individual change.
**Better**:
```
PATCH /v1/recipes
{
"changes": [{
"recipe_id": "lungo",
"volume": "300ml"
}, {
"recipe_id": "latte",
"volume": "-1ml"
}]
}
// You may actually return
// a ‘partial success’ status
// if the protocol allows it
→ 200 OK
{
"changes": [{
"change_id",
"occurred_at",
"recipe_id": "lungo",
"status": "success"
}, {
"change_id",
"occurred_at",
"recipe_id": "latte",
"status": "fail",
"error"
}]
}
```
Here:
* `change_id` is a unique identifier of each atomic change;
* `occurred_at` is a moment of time when the change was actually applied;
* `error` field contains the error data related to the specific change.
Might be of use:
* introducing `sequence_id` parameters in the request to guarantee execution order and to align item order in response with the requested one;
* expose a separate `/changes-history` endpoint for clients to get the history of applied changes even if the app crashed while getting partial success response or there was a network timeout.
Non-atomic changes are undesirable because they erode the idempotency concept. Let's take a look at the example:
```
PATCH /v1/recipes
{
"idempotency_token",
"changes": [{
"recipe_id": "lungo",
"volume": "300ml"
}, {
"recipe_id": "latte",
"volume": "400ml"
}]
}
→ 200 OK
{
"changes": [{
"status": "success"
}, {
"status": "fail",
"error": {
"reason": "too_many_requests"
}
}]
}
```
Imagine the client failed to get a response because of a network error, and it repeats the request:
```
PATCH /v1/recipes
{
"idempotency_token",
"changes": [{
"recipe_id": "lungo",
"volume": "300ml"
}, {
"recipe_id": "latte",
"volume": "400ml"
}]
}
→ 200 OK
{
"changes": [{
"status": "success"
}, {
"status": "success",
}]
}
```
To the client, everything looks normal: changes were applied, and the last response got is always actual. But the resource state after the first request was inherently different from the resource state after the second one, which contradicts the very definition of ‘idempotency’.
It would be more correct if the server did nothing upon getting the second request with the same idempotency token, and returned the same status list breakdown. But it implies that storing these breakdowns must be implemented.
Just in case: nested operations must be idempotent themselves. If they are not, separate idempotency tokens must be generated for each nested operation.
##### Specify caching policies
Client-server interaction usually implies that network and server resources are limited, therefore caching operation results on client devices is a standard practice.
@ -309,7 +642,8 @@ So it's highly desirable to make caching options clear, if not from functions' s
```
// Returns lungo price in cafes
// closest to the specified location
GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
GET /price?recipe=lungo
&longitude={longitude}&latitude={latitude}
{ "currency_code", "price" }
```
@ -321,7 +655,8 @@ Two questions arise:
```
// Returns an offer: for what money sum
// our service commits to make a lungo
GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
GET /price?recipe=lungo
&longitude={longitude}&latitude={latitude}
{
"offer": {
@ -341,7 +676,7 @@ GET /price?recipe=lungo&longitude={longitude}&latitude={latitude}
}
```
##### 11. Pagination, filtration, and cursors
##### Pagination, filtration, and cursors
Any endpoints returning data collections must be paginated. No exclusions exist.
@ -463,7 +798,7 @@ POST /v1/records/modified/list
This scheme's downsides are the necessity to create separate indexed event storage, and the multiplication of data items, since for a single record many events might exist.
##### 12. Errors must be informative
##### Errors must be informative
While writing the code developers face problems, many of them quite trivial, like invalid parameter type or some boundary violation. The more convenient are error responses your API return, the less time developers waste in struggling with it, and the more comfortable is working with the API.
@ -512,7 +847,141 @@ POST /v1/coffee-machines/search
```
It is also a good practice to return all detectable errors at once to spare developers' time.
##### 13. Localization and internationalization
##### Maintain a proper error sequence
In first, always return unresolvable errors before re resolvable once:
```
POST /v1/orders
{
"recipe": "lngo",
"offer"
}
→ 409 Conflict
{
"reason": "offer_expired"
}
// Request repeats
// with the renewed offer
POST /v1/orders
{
"recipe": "lngo",
"offer"
}
→ 400 Bad Request
{
"reason": "recipe_unknown"
}
```
— what was the point of renewing the offer if the order cannot be created anyway?
In second, maintain such a sequence of unresolvable errors which leads to a minimal amount of customers' and developers' irritation.
**Bad**:
```
POST /v1/orders
{
"items": [{ "item_id": "123", "price": "0.10" }]
}
409 Conflict
{
"reason": "price_changed",
"details": [{ "item_id": "123", "actual_price": "0.20" }]
}
// Request repeats
// with an actual price
POST /v1/orders
{
"items": [{ "item_id": "123", "price": "0.20" }]
}
409 Conflict
{
"reason": "order_limit_exceeded",
"localized_message": "Order limit exceeded"
}
```
— what was the point of showing the price changed dialog, if the user still can't make an order, even if the price is right? When one of the concurrent orders finishes, and the user is able to commit another one, prices, items availability, and other order parameters will likely need another correction.
In third, draw a chart: which error resolution might lead to the emergence of another one. Otherwise you might eventually return the same error several time, or worse, make a cycle of errors.
```
// Create an order
// with a payed delivery
POST /v1/orders
{
"items": 3,
"item_price": "3000.00"
"currency_code": "MNT",
"delivery_fee": "1000.00",
"total": "10000.00"
}
→ 409 Conflict
// Error: if the order sum
// is more than 9000 tögrögs,
// delivery must be free
{
"reason": "delivery_is_free"
}
// Create an order
// with a free delivery
POST /v1/orders
{
"items": 3,
"item_price": "3000.00"
"currency_code": "MNT",
"delivery_fee": "0.00",
"total": "9000.00"
}
→ 409 Conflict
// Error: munimal order sum
// is 10000 tögrögs
{
"reason": "below_minimal_sum",
"currency_code": "MNT",
"minimal_sum": "10000.00"
}
```
You may note that in this setup the error can't resolved in one step: this situation must be elaborated over, and either order calculation parameters must be changed (discounts should not be counted against the minimal order sum), or a special type of error must be introduced.
##### No results is a result
If a server processed a request correctly and no exceptional situation occurred — there must be no error. Regretfully, an antipattern is widespread — of throwing errors when zero results are found .
**Bad**
```
POST /search
{
"query": "lungo",
"location": <customer's location>
}
→ 404 Not Found
{
"localized_message":
"No one makes lungo nearby"
}
```
`4xx` statuses imply that a client made a mistake. But no mistakes were made by either a customer or a developer: a client cannot know whether the lungo is served in this location beforehand.
**Better**:
```
POST /search
{
"query": "lungo",
"location": <customer's location>
}
→ 200 OK
{
"results": []
}
```
This rule might be reduced to: if an array is the result of the operation, than emptiness of that array is not a mistake, but a correct response. (Of course, if empty array is acceptable semantically; empty coordinates array is a mistake, of course.)
##### Localization and internationalization
All endpoints must accept language parameters (for example, in a form of the `Accept-Language` header), even if they are not being used currently.

View File

@ -1,4 +1,4 @@
<h1>Sergey Konstantinov<br />The API</h1>
<div class="cover"><h1>Sergey Konstantinov<br />The API</h1></div>
<img class="cc-by-nc-img" src="https://i.creativecommons.org/l/by-nc/4.0/88x31.png" />
<p class="cc-by-nc">

View File

@ -14,7 +14,7 @@ Rule \#1 is the simplest: if some functionality might be withheld — then never
##### 2. Avoid gray zones and ambiguities
You obligations to maintain some functionality must be stated as clearly as possible. Especially regarding those environments and platforms where no native capability to restrict access to undocumented functionality exists. Unfortunately, developers tend to consider some private features they found to be eligible for use, thus presuming the API vendor shall maintain it intact. Policy on such ‘findings’ must be articulated explicitly. At the very least, in case of such non-authorized usage of undocumented functionality, you might refer to the docs, and be in your own rights in the eyes of the community.
Your obligations to maintain some functionality must be stated as clearly as possible. Especially regarding those environments and platforms where no native capability to restrict access to undocumented functionality exists. Unfortunately, developers tend to consider some private features they found to be eligible for use, thus presuming the API vendor shall maintain them intact. Policy on such ‘findings’ must be articulated explicitly. At the very least, in case of such non-authorized usage of undocumented functionality, you might refer to the docs, and be in your own rights in the eyes of the community.
However, API developers often legitimize such gray zones themselves, for example, by:
@ -25,7 +25,7 @@ One cannot make a partial commitment. Either you guarantee this code will always
##### 3. Codify implicit agreements
Third principle is much less obvious. Pay a close attention to the code which you're suggesting developers to write: are there any conventions which you consider evident, but never wrote them down?
Third principle is much less obvious. Pay close attention to the code which you're suggesting developers to write: are there any conventions which you consider evident, but never wrote them down?
**Example \#1**. Let's take a look at this order processing SDK example:
```
@ -39,7 +39,7 @@ Let's imagine that you're struggling with scaling your service, and at some poin
What would be the result? The code above will stop working. A developer creates an order, tries to get its status — but gets the error. It's very hard to predict what an approach developers would implement to tackle this error. Probably, none at all.
You may say something like, ‘But we've never promised the strict consistency in the first place’ — and that is obviously not true. You may say that if, and only if, you really described the eventual consistency in the `createOrder` docs, and all your SDK examples look like:
You may say something like, ‘But we've never promised the strict consistency in the first place’ — and that is obviously not true. You may say that if, and only if, you have really described the eventual consistency in the `createOrder` docs, and all your SDK examples look like:
```
let order = api.createOrder();
@ -60,7 +60,7 @@ if (status) {
We presume we may skip the explanations why such code must never be written in any circumstances. If you're really providing a non-strictly consistent API, then either `createOrder` operation must be asynchronous and return the result when all replicas are synchronized, or the retry policy must be hidden inside `getStatus` operation implementation.
If you failed to describe the eventual consistency in the first place, then you simply can't make these changes in the API. You will effectively break backwards compatibility, which will lead to huge problems with your customers' apps, intensified by the fact it can't be simply reproduced.
If you failed to describe the eventual consistency in the first place, then you simply can't make these changes in the API. You will effectively break backwards compatibility, which will lead to huge problems with your customers' apps, intensified by the fact they can't be simply reproduced.
**Example \#2**. Take a look at the following code:
@ -74,7 +74,7 @@ let promise = new Promise(
resolve();
```
This code presumes that callback function passed to `new Promise` will be executed synchronously, and the `resolve` variable will be initialized before the `resolve()` function is called. But this convention is based on nothing: there is no clues indicating the `new Promise` constructor executes the callback function synchronously.
This code presumes that callback function passed to `new Promise` will be executed synchronously, and the `resolve` variable will be initialized before the `resolve()` function is called. But this assumption is based on nothing: there are no clues indicating that `new Promise` constructor executes the callback function synchronously.
Of course, the developers of the language standard can afford such tricks; but you as an API developer cannot. You must at least document this behavior and make the signatures point to it; actually, good advice is to avoid such conventions, since they are simply unobvious while reading the code. And of course, under no circumstances you actually change this behavior to asynchronous one.
@ -91,7 +91,7 @@ object.observe('widthchange', observerFunction);
A question arises: how frequently and at what time fractions the `observerFunction` will be called? Let's assume in the first SDK version we emulated step-by-step animation at 10 frames per second: then `observerFunction` will be called 10 times, getting values '140px', '180px', etc., up to '500px'. But then in new API version we moved to implementing both functions atop of system native functionality — and so you're simply don't know, when and how frequently the `observerFunction` will be called.
Just changing call frequency might result in making some code dysfunctional — for example, if the callback function makes some complex calculations, and no throttling is implemented, since the developer just relied on your SDK built-in throttling. An if `observerFunction` cease to be called when exactly '500px' is reached because of some system algorithms specifics, some code will be broken without any doubt.
Just changing call frequency might result in making some code dysfunctional — for example, if the callback function makes some complex calculations, and no throttling is implemented, since the developer just relied on your SDK built-in throttling. An if `observerFunction` cease to be called when exactly '500px' is reached because of some system algorithms specifics, some code will be broken beyond any doubt.
In this example you should document the concrete contract (how often the observer function is called) and stick to it even if the underlying technology is changed.
@ -122,7 +122,7 @@ GET /v1/orders/{id}/events/history
}
```
Suppose at some moment we decided to allow trustworthy clients to get their coffee in advance, before the payment is confirmed. So an order will jump straight to "preparing_started", or event "ready", without a "payment_approved" event being emitted. It might appear to you that this modification is backwards compatible, since you never really promised any specific event order be maintained, but it is not.
Suppose at some moment we decided to allow trustworthy clients to get their coffee in advance, before the payment is confirmed. So an order will jump straight to "preparing_started", or event "ready", without a "payment_approved" event being emitted. It might appear to you that this modification *is* backwards compatible, since you've never really promised any specific event order be maintained, but it is not.
Let's assume that a developer (probably, your company's business partner) wrote some code executing some valuable business procedure, for example, gathering income and expenses analytics. It's quite logical to expect this code operates a state machine, which switches from one state to another depending on getting (or getting not) specific events. This analytical code will be broken if the event order changes. In best-case scenario a developer will get some exceptions and have to cope with error's cause; worst-case, partners will operate wrong statistics for an indefinite period of time until they find a mistake.

View File

@ -275,33 +275,43 @@ if (Type(order.contactless_delivery) == 'Boolean' &&
**Плохо**:
```
// Задаёт лимит количества
// одновременных заказов
// на пользователя, числом
// либо null — лимита нет
PUT /users/{id}/order-limit
{}
// Создаёт пользователя
POST /users
{ … }
{ "order_limit": null }
// Пользователи создаются по умолчанию
// с указанием лимита трат в месяц
{
"spendings_monthly_limit_usd": "100"
}
// Для отмены лимита требуется
// указать значение null
POST /users
{
"spendings_monthly_limit_usd": null
}
```
**Хорошо**
```
// Задаёт флаг наличия/отсутствия
// лимита заказов, и его значение
// при наличии
PUT /users/{id}/order-limit
POST /users
{
// true — у пользователя задан
// лимит одновременных заказов
// false — лимита нет
// true — у пользователя снят
// лимит трат в месяц
// false — лимит не снят
// (значение по умолчанию)
"has_specific_limit": true,
"limit": 5
"abolish_spendings_limit": false,
// Необязательное поле, имеет смысл
// только если предыдущий флаг
// имеет значение false
"spendings_monthly_limit_usd": "100",
}
```
**NB**: противоречие с предыдущим советом в том, что мы специально ввели отрицающий флаг («нет лимита»), который по правилу двойных отрицаний пришлось переименовать в `has_specific_limit`. Хотя это и хорошее название для отрицательного флага, семантика его довольно неочевидна, разработчикам придётся как минимум покопаться в документации. Таков путь.
**NB**: противоречие с предыдущим советом в том, что мы специально ввели отрицающий флаг («нет лимита»), который по правилу двойных отрицаний пришлось переименовать в `abolish_spendings_limit`. Хотя это и хорошее название для отрицательного флага, семантика его довольно неочевидна, разработчикам придётся как минимум покопаться в документации. Таков путь.
##### Избегайте частичных обновлений
@ -328,7 +338,7 @@ PATCH /v1/orders/123
Во-первых, экономия объёма ответа в современных условиях требуется крайне редко. Максимальные размеры сетевых пакетов (MTU, Maximum Transmission Unit) в настоящее время составляют более килобайта; пытаться экономить на размере ответа, пока он не превышает килобайт — попросту бессмысленная трата времени. Да и в целом, скорее более оправдан следующий подход: для тяжёлых данных следует сделать отдельный эндпойнт, а на всём остальном не пытаться экономить.
Во-вторых, экономия размера ответа как раз сыграет злую шутку при совместном редактировании: один клиент не будет видеть, какие изменения внёс другой. Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа не оказывает значительного влияния на производительность) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.
Во-вторых, экономия размера ответа выйдет боком как раз при совместном редактировании: один клиент не будет видеть, какие изменения внёс другой. Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа не оказывает значительного влияния на производительность) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.
В-третьих, этот подход может как-то работать при необходимость перезаписать поле. Но что делать, если поле требуется сбросить к значению по умолчанию? Например, как *удалить* `client_phone_number_ext`?
@ -462,7 +472,7 @@ X-Idempotency-Token: <токен>
```
— сервер обнаружил, что ревизия 123 была создана с другим токеном, значит имеет место быть конфликт общего доступа к ресурсу.
Более того, добавление токена идемпотентности не только решает эту проблему, но и позволяет в будущем сделать продвинутые оптимизации. Если сервер обнаруживает конфликт общего доступа, он может попытаться решить его, «перебазировав» обновление, как это делают современные системы контроля версий, и вернуть `200 OK` вместо `409 Conflict`. Эта логика существенно улучшает пользовательский опыт и при этом полностью обратно совместима (если, конечно, вы следовали правилу \#9 при разработке API) и предотвращает фрагментацию кода разрешения конфликтов.
Более того, добавление токена идемпотентности не только решает эту проблему, но и позволяет в будущем сделать продвинутые оптимизации. Если сервер обнаруживает конфликт общего доступа, он может попытаться решить его, «перебазировав» обновление, как это делают современные системы контроля версий, и вернуть `200 OK` вместо `409 Conflict`. Эта логика существенно улучшает пользовательский опыт и при этом полностью обратно совместима и предотвращает фрагментацию кода разрешения конфликтов.
Но имейте в виду: клиенты часто ошибаются при имплементации логики токенов идемпотентности. Две проблемы проявляются постоянно:
* нельзя полагаться на то, что клиенты генерируют честные случайные токены — они могут иметь одинаковый seed рандомизатора или просто использовать слабый алгоритм или источник энтропии; при проверке токенов нужны слабые ограничения: уникальность токена должна проверяться не глобально, а только применительно к конкретному пользователю и конкретной операции;
@ -553,7 +563,7 @@ PATCH /v1/recipes
Здесь:
* `change_id` — уникальный идентификатор каждого атомарного изменения;
* `occurred_at` — время проведения каждого изменения
* `occurred_at` — время проведения каждого изменения;
* `error` — информация по ошибке для каждого изменения, если она возникла.
Не лишним будет также: