@@ -598,7 +598,7 @@ ul.references li p a.back-anchor { This book is distributed under the Creative Commons Attribution-NonCommercial 4.0 International licence. Source code available at github.com/twirl/The-API-Book - Share: facebook · twitter · linkedin · redditTable of ContentsIntroductionChapter 1. On the Structure of This BookChapter 2. The API DefinitionChapter 3. Overview of Existing API Development SolutionsChapter 4. API Quality CriteriaChapter 5. The API-First ApproachChapter 6. On Backward CompatibilityChapter 7. On VersioningChapter 8. Terms and Notation KeysSection I. The API DesignChapter 9. The API Contexts PyramidChapter 10. Defining an Application FieldChapter 11. Separating Abstraction LevelsChapter 12. Isolating Responsibility AreasChapter 13. Describing Final InterfacesChapter 14. Annex to Section I. Generic API Example[Work in Progress] Section II. The API PatternsChapter 15. On Design Patterns in the API ContextChapter 16. Authenticating Partners and Authorizing API CallsChapter 17. Synchronization StrategiesChapter 18. Eventual ConsistencyChapter 19. Asynchronicity and Time ManagementChapter 20. Lists and Accessing ThemChapter 21. Bidirectional Data Flows. Push and Poll ModelsChapter 22. Multiplexing Notifications. Asynchronous Event ProcessingChapter 23. Atomicity of Bulk ChangesChapter 24. Partial UpdatesChapter 25. Degradation and PredictabilitySection III. The Backward CompatibilityChapter 26. The Backward Compatibility Problem StatementChapter 27. On the Waterline of the IcebergChapter 28. Extending through AbstractingChapter 29. Strong Coupling and Related ProblemsChapter 30. Weak CouplingChapter 31. Interfaces as a Universal PatternChapter 32. The Serenity Notepad[Work in Progress] Section IV. The HTTP API & RESTChapter 33. On HTTP API Concept and TerminologyChapter 34. The REST MythChapter 35. The Semantics of the HTTP Request ComponentsChapter 36. The HTTP API Advantages and DisadvantagesChapter 37. HTTP API Organization PrinciplesChapter 38. Working with HTTP API ErrorsChapter 39. Organizing the HTTP API Resources and OperationsChapter 40. Final Provisions and General Recommendations[Work in Progress] Section V. The SDK & UI LibrariesChapter 41. On the Content of This SectionChapter 42. The SDK: Problems and SolutionsChapter 43. The Code Generation PatternChapter 44. The UI ComponentsChapter 45. Decomposing UI ComponentsChapter 46. The MV* FrameworksChapter 47. The Backend-Driven UIChapter 48. Shared Resources and Asynchronous LocksChapter 49. Computed PropertiesChapter 50. ConclusionSection VI. The API ProductChapter 51. API as a ProductChapter 52. The API Business ModelsChapter 53. Developing a Product VisionChapter 54. Communicating with DevelopersChapter 55. Communicating with Business OwnersChapter 56. The API Services RangeChapter 57. The API Key Performance IndicatorsChapter 58. Identifying Users and Preventing FraudChapter 59. The Technical Means of Preventing ToS ViolationsChapter 60. Supporting customersChapter 61. The DocumentationChapter 62. The Testing EnvironmentChapter 63. Managing Expectations + Share: facebook · twitter · linkedin · redditTable of ContentsIntroductionChapter 1. On the Structure of This BookChapter 2. The API DefinitionChapter 3. Overview of Existing API Development SolutionsChapter 4. API Quality CriteriaChapter 5. The API-First ApproachChapter 6. On Backward CompatibilityChapter 7. On VersioningChapter 8. Terms and Notation KeysSection I. The API DesignChapter 9. The API Contexts PyramidChapter 10. Defining an Application FieldChapter 11. Separating Abstraction LevelsChapter 12. Isolating Responsibility AreasChapter 13. Describing Final InterfacesChapter 14. Annex to Section I. Generic API Example[Work in Progress] Section II. The API PatternsChapter 15. On Design Patterns in the API ContextChapter 16. Authenticating Partners and Authorizing API CallsChapter 17. Synchronization StrategiesChapter 18. Eventual ConsistencyChapter 19. Asynchronicity and Time ManagementChapter 20. Lists and Accessing ThemChapter 21. Bidirectional Data Flows. Push and Poll ModelsChapter 22. Multiplexing Notifications. Asynchronous Event ProcessingChapter 23. Atomicity of Bulk ChangesChapter 24. Partial UpdatesChapter 25. Degradation and PredictabilitySection III. The Backward CompatibilityChapter 26. The Backward Compatibility Problem StatementChapter 27. On the Waterline of the IcebergChapter 28. Extending through AbstractingChapter 29. Strong Coupling and Related ProblemsChapter 30. Weak CouplingChapter 31. Interfaces as a Universal PatternChapter 32. The Serenity Notepad[Work in Progress] Section IV. The HTTP API & RESTChapter 33. On HTTP API Concept and TerminologyChapter 34. The REST MythChapter 35. The Semantics of the HTTP Request ComponentsChapter 36. The HTTP API Advantages and DisadvantagesChapter 37. HTTP API Organization PrinciplesChapter 38. Working with HTTP API ErrorsChapter 39. Organizing the HTTP API Resources and OperationsChapter 40. Final Provisions and General Recommendations[Work in Progress] Section V. The SDK & UI LibrariesChapter 41. On the Content of This SectionChapter 42. The SDK: Problems and SolutionsChapter 43. The Code Generation PatternChapter 44. The UI ComponentsChapter 45. Decomposing UI ComponentsChapter 46. The MV* FrameworksChapter 47. The Backend-Driven UIChapter 48. Shared Resources and Asynchronous LocksChapter 49. Computed PropertiesChapter 50. ConclusionSection VI. The API ProductChapter 51. API as a ProductChapter 52. The API Business ModelsChapter 53. Developing a Product VisionChapter 54. Communicating with DevelopersChapter 55. Communicating with Business OwnersChapter 56. The API Services RangeChapter 57. The API Key Performance IndicatorsChapter 58. Identifying Users and Preventing FraudChapter 59. The Technical Means of Preventing ToS ViolationsChapter 60. Supporting customersChapter 61. The DocumentationChapter 62. The Testing EnvironmentChapter 63. Managing Expectations § @@ -1804,7 +1804,7 @@ PUT /v1/users/{id} All these problems must be addressed by setting limitations on field sizes and properly decomposing endpoints. If an entity comprises both “lightweight” data (such as the name and description of a recipe) and “heavy” data (such as the promotional picture of a beverage which might easily be a hundred times larger than the text fields), it's better to split endpoints and pass only a reference to the “heavy” data (e.g., a link to the image). This will also allow for setting different cache policies for different kinds of data. As a useful exercise, try modeling the typical lifecycle of a partner's app's main functionality (e.g., making a single order) to count the number of requests and the amount of traffic it requires. It might turn out that the high number of requests or increased network traffic consumption is due to a mistake in the design of state change notification endpoints. We will discuss this issue in detail in the “Bidirectional Data Flow” chapter of “The API Patterns” section of this book. 14. No Results Is a Result -If a server processed a request correctly and no exceptional situation occurred — there must be no error. Regretfully, the antipattern is widespread — of throwing errors when no results are found. +If a server processes a request correctly and no exceptional situation occurs, there should be no error. Unfortunately, the antipattern of throwing errors when no results are found is widespread. Bad POST /v1/coffee-machines/search { @@ -1817,7 +1817,7 @@ PUT /v1/users/{id} "No one makes lungo nearby" } -4xx statuses imply that a client made a mistake. But no mistakes were made by either the customer or the developer: a client cannot know whether the lungo is served in this location beforehand. +The response implies that a client made a mistake. However, in this case, neither the customer nor the developer made any mistakes. The client cannot know beforehand whether lungo is served in this location. Better: POST /v1/coffee-machines/search { @@ -1829,8 +1829,8 @@ PUT /v1/users/{id} "results": [] } -This rule might be reduced to: if an array is the result of the operation, then the emptiness of that array is not a mistake, but a correct response. (Of course, if an empty array is acceptable semantically; an empty array of coordinates is a mistake for sure.) -NB: this pattern should be applied in the opposite case as well. If an array of entities might be am optional parameter to the request, the empty array and the absence of the field must be treated differently. Let's take a look at the example: +This rule can be summarized as follows: if an array is the result of the operation, then the emptiness of that array is not a mistake, but a correct response. (Of course, this applies if an empty array is semantically acceptable; an empty array of coordinates, for example, would be a mistake.) +NB: this pattern should also be applied in the opposite case. If an array of entities is an optional parameter in the request, the empty array and the absence of the field must be treated differently. Let's consider the example: // Finds all coffee recipes // that contain no milk POST /v1/recipes/search @@ -1860,17 +1860,17 @@ POST /v1/offers/search ] } -Let's imagine that the first request returned an empty array of results, i.e., there are no known recipes that satisfy the condition. Of course, would be nice if the developer expected this situation and installed a guard that prohibits the call to the offer search function in this case — but we can't be 100% sure they did. If this logic is missing, the application will make the following call: +Now let's imagine that the first request returned an empty array of results meaning there are no known recipes that satisfy the condition. Ideally, the developer would have expected this situation and installed a guard to prevent the call to the offer search function in this case. However, we can't be 100% sure they did. If this logic is missing, the application will make the following call: POST /v1/offers/search { "location", "recipes": [] } -Often, the endpoint implementation ignores the empty recipe array and returns a list of offers just like no recipe filter was supplied. In our case, it means that the application seemingly ignores the user's request to show only milk-free beverages, which we can't consider acceptable behavior. Therefore, the response to such a request with an empty array parameter should be either an error or an empty result. +Often, the endpoint implementation ignores the empty recipe array and returns a list of offers as if no recipe filter was supplied. In our case, it means that the application seemingly ignores the user's request to show only milk-free beverages, which we consider unacceptable behavior. Therefore, the response to such a request with an empty array parameter should either be an error or an empty result. 15. Validate Inputs -The decision of which of the options to choose in the previous example, an exception or an empty response, directly depends on what's stated in the contract. If the specification prescribes that the recipes parameter must not be empty, an error shall be generated (otherwise you violate your own spec). -This rule applies not only to empty arrays but to every restriction stipulated in the contract. “Silent” fixing of invalid values rarely bears practical sense: +The decision of whether to use an exception or an empty response in the previous example depends directly on what is stated in the contract. If the specification specifies that the recipes parameter must not be empty, an error should be generated (otherwise, you would violate your own spec). +This rule applies not only to empty arrays but to every restriction specified in the contract. “Silently” fixing invalid values rarely makes practical sense. Bad: POST /v1/offers/search { @@ -1886,7 +1886,7 @@ POST /v1/offers/search "offers" } -As we can see, the developer somehow passed the wrong latitude value (100 degrees). Yes, we can “fix” it, e.g., reduce it to the closest valid value, which is 90 degrees, but who got benefitted from this? The developer will never learn about this mistake, and we doubt that Northern Pole coffee offers are relevant to users. +As we can see, the developer somehow passed the wrong latitude value (100 degrees). Yes, we can “fix” it by reducing it to the closest valid value, which is 90 degrees, but who benefits from this? The developer will never learn about this mistake, and we doubt that coffee offers in the Northern Pole vicinity are relevant to users. Better: POST /v1/coffee-machines/search { @@ -1900,7 +1900,7 @@ POST /v1/offers/search // Error description } -It is also useful to proactively notify partners about the behavior that looks like a mistake: +It is also useful to proactively notify partners about behavior that appears to be a mistake: POST /v1/coffee-machines/search { "location": { @@ -1923,7 +1923,7 @@ POST /v1/offers/search }] } -As it might happen that adding such notices is not possible, we might introduce the debug mode or strict mode, in which notices are escalated: +If it is not possible to add such notices, we can introduce a debug mode or strict mode in which notices are escalated: POST /v1/coffee-machines/search⮠ strict_mode=true { @@ -1948,7 +1948,7 @@ POST /v1/offers/search disable_errors=suspicious_coordinates 16. Default Values Must Make Sense -Setting default values is one of the most powerful tools that help in avoiding many-wordiness while working with APIs. However, these values must help developers, not hide their mistakes. +Setting default values is one of the most powerful tools that help avoid verbosity when working with APIs. However, these values should help developers rather than hide their mistakes. Bad: POST /v1/coffee-machines/search { @@ -1963,7 +1963,7 @@ POST /v1/offers/search ] } -Formally speaking, having such a behavior is feasible: why not have a “default geographical coordinates” concept? In the reality, however, such policies of “silent” fixing of mistakes lead to absurd situations like “the null island” — the most visited place in the world. The more popular an API, the more chances partners just overlook these edge cases. +Formally speaking, having such behavior is feasible: why not have a “default geographical coordinates” concept? However, in reality, such policies of “silently” fixing mistakes lead to absurd situations like “the null island” — the most visited place in the world. The more popular an API becomes, the higher the chances that partners will overlook these edge cases. Better: POST /v1/coffee-machines/search { @@ -1976,7 +1976,7 @@ POST /v1/offers/search } 17. Errors Must Be Informative -It is not enough to just validate inputs; describing the cause of the error properly is also a must. While writing code developers face problems, many of them quite trivial, like invalid parameter types or some boundary violations. The more convenient the error responses your API return, the less the amount of time developers waste struggling with it, and the more comfortable working with the API. +It is not enough to simply validate inputs; providing proper descriptions of errors is also essential. When developers write code, they encounter problems, sometimes quite trivial, such as invalid parameter types or boundary violations. The more convenient the error responses returned by your API, the less time developers will waste struggling with them, and the more comfortable working with the API will be for them. Bad: POST /v1/coffee-machines/search { @@ -1989,7 +1989,7 @@ POST /v1/offers/search → 400 Bad Request {} -— of course, the mistakes (typo in the "lngo", wrong coordinates) are obvious. But the handler checks them anyway, so why not return readable descriptions? +— of course, the mistakes (typo in "lngo", wrong coordinates) are obvious. But the handler checks them anyway, so why not return readable descriptions? Better: { "reason": "wrong_parameter_value", @@ -2022,7 +2022,7 @@ POST /v1/offers/search } } -It is also a good practice to return all detectable errors at once to spare developers time. +It is also a good practice to return all detectable errors at once to save developers time. 18. Return Unresolvable Errors First POST /v1/orders { @@ -2045,9 +2045,9 @@ POST /v1/orders "reason": "recipe_unknown" } -— what was the point of renewing the offer if the order cannot be created anyway? For the user, it will look like meaningless efforts (or meaningless waiting) that will anyway result in an error, whatever they do. Yes, maintaining errors priorities won't change the result — the order still cannot be created — but, first, users will spend less time (also, make fewer mistakes and contribute less to the error metrics) and, second, diagnostic logs for the problem will be much easier readable. -19. Resolve Error Starting With Big Ones -If the errors under consideration are resolvable (i.e., the user might carry on some actions and still get what they need), you should first notify them of those errors that will require more significant state update. +— what was the point of renewing the offer if the order cannot be created anyway? For the user, it will look like meaningless efforts (or meaningless waiting) that will ultimately result in an error regardless of what they do. Yes, maintaining error priorities won't change the result — the order still cannot be created. However, first, users will spend less time (also make fewer mistakes and contribute less to the error metrics) and second, diagnostic logs for the problem will be much easier to read. +19. Prioritize Significant Errors +If the errors under consideration are resolvable (i.e., the user can take some actions and still get what they need), you should first notify them of those errors that will require more significant state updates. Bad: POST /v1/orders { @@ -2093,7 +2093,7 @@ POST /v1/orders 20. Analyze Potential Error Deadlocks In complex systems, it might happen that resolving one error leads to another one, and vice versa. // Create an order -// with a paid delivery +// with paid delivery POST /v1/orders { "items": 3, @@ -2110,7 +2110,7 @@ POST /v1/orders "reason": "delivery_is_free" } // Create an order -// with a free delivery +// with free delivery POST /v1/orders { "items": 3, @@ -2120,7 +2120,7 @@ POST /v1/orders "total": "9000.00" } → 409 Conflict -// Error: minimal order sum +// Error: the minimal order sum // is 10000 tögrögs { "reason": "below_minimal_sum", @@ -2128,10 +2128,10 @@ POST /v1/orders "minimal_sum": "10000.00" } -You may note that in this setup the error can't be 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. +You may note that in this setup the error can't be resolved in one step: this situation must be elaborated on, 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. 21. Specify Caching Policies and Lifespans of Resources -In modern systems, clients usually have their own state and almost universally cache results of requests — no matter, session-wise or long-term, every entity has some period of autonomous existence. So it's highly desirable to make clarifications; it should be understandable how the data is supposed to be cached, if not from operation signatures, but at least from the documentation. -Let's stress that we understand “cache” in the extended sense: which variation of operation parameters (not just the request time, but other variables as well) should be considered close enough to some previous request to use the cached result? +In modern systems, clients usually have their own state and almost universally cache results of requests. Every entity has some period of autonomous existence, whether session-wise or long-term. So it's highly desirable to provide clarifications: it should be understandable how the data is supposed to be cached, if not from operation signatures, but at least from the documentation. +Let's emphasize that we understand “cache” in the extended sense: which variations of operation parameters (not just the request time, but other variables as well) should be considered close enough to some previous request to use the cached result? Bad: // Returns lungo prices including // delivery to the specified location @@ -2143,10 +2143,10 @@ GET /price?recipe=lungo⮠ Two questions arise: -until when the price is valid? -in what vicinity of the location the price is valid? +Until when is the price valid? +In what vicinity of the location is the price valid? -Better: you may use standard protocol capabilities to denote cache options, like the Cache-Control header. If you need caching in both temporal and spatial dimensions, you should do something like that: +Better: you may use standard protocol capabilities to denote cache options, such as the Cache-Control header. If you need caching in both temporal and spatial dimensions, you should do something like this: GET /price?recipe=lungo⮠ &longitude={longitude}⮠ &latitude={latitude} @@ -2159,8 +2159,8 @@ GET /price?recipe=lungo⮠ "conditions": { // Until when the price is valid "valid_until", - // What vicinity - // the price is valid within + // In what vicinity + // the price is valid // * city // * geographical object // * … @@ -2169,13 +2169,14 @@ GET /price?recipe=lungo⮠ } } +NB: sometimes, developers set very long caching times for immutable resources, spanning a year or even more. It makes little practical sense as the server load will not be significantly reduced compared to caching for, let's say, one month. However, the cost of a mistake increases dramatically: if wrong data is cached for some reason (for example, a 404 error), this problem will haunt you for the next year or even more. We would recommend selecting reasonable cache parameters based on how disastrous invalid caching would be for the business. 22. Keep the Precision of Fractional Numbers 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. -If conversion to a float number will certainly lead to losing the precision (let's say if we translate “20 minutes” into hours as a decimal fraction), it's better to either stick to a fully precise format (e.g., opt for 00:20 instead of 0.33333…), or provide an SDK to work with this data, or as a last resort describe the rounding principles in the documentation. +If the protocol allows, fractional numbers with fixed precision (such as 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 multiplier) or strings. +If converting to a float number will certainly lead to a loss of precision (for example, if we translate “20 minutes” into hours as a decimal fraction), it's better to either stick to a fully precise format (e.g., use 00:20 instead of 0.33333…), or provide an SDK to work with this data. As a last resort, describe the rounding principles in the documentation. 23. All API Operations Must Be Idempotent -Let us remind the reader that idempotency is the following property: repeated calls to the same function with the same parameters won't change the resource state. Since we're discussing client-server interaction in the first place, repeating requests in case of network failure isn't an exception, but a norm of life. -If the 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. +Let us remind the reader that idempotency is the following property: repeated calls to the same function with the same parameters won't change the resource state. Since we are primarily discussing client-server interaction, repeating requests in case of network failure is not something exceptional but a common occurrence. +If an endpoint's idempotency can not be naturally assured, explicit idempotency parameters must be added in the form of a token or a resource version. Bad: // Creates an order POST /orders @@ -2186,8 +2187,8 @@ POST /orders POST /v1/orders X-Idempotency-Token: <random string> -A client on its side must retain the X-Idempotency-Token in case of automated endpoint retrying. A server on its side must check whether an order created with this token exists. -An alternative: +The client must retain the X-Idempotency-Token in case of automated endpoint retrying. The server must check whether an order created with this token already exists. +Alternatively: // Creates order draft POST /v1/orders/drafts → @@ -2198,23 +2199,22 @@ PUT /v1/orders/drafts⮠ /{draft_id}/confirmation { "confirmed": true } -Creating order drafts is a non-binding operation since it doesn't entail any consequences, so it's fine to create drafts without the idempotency token. -Confirming drafts is a naturally idempotent operation, with the draft_id being its idempotency key. -Also worth mentioning that adding idempotency tokens to naturally idempotent handlers isn't meaningless either, since it allows to distinguish two situations: +Creating order drafts is a non-binding operation as it doesn't entail any consequences, so it's fine to create drafts without the idempotency token. Confirming drafts is a naturally idempotent operation, with the draft_id serving as its idempotency key. +It is also worth mentioning that adding idempotency tokens to naturally idempotent handlers is not meaningless. It allows distinguishing between two situations: -a client didn't get the response because of some network issues, and is now repeating the request; -a client made a mistake by posting conflicting requests. +The client did not receive the response due to network issues and is now repeating the request. +The client made a mistake by posting conflicting requests. -Consider the following example: imagine there is a shared resource, characterized by a revision number, and a client tries updating it. +Consider the following example: imagine there is a shared resource, characterized by a revision number, and the client tries to update it. POST /resource/updates { "resource_revision": 123 "updates" } -The server retrieves the actual resource revision and finds it to be 124. How to respond correctly? The 409 Conflict code might be returned, but then the client will be forced to understand the nature of the conflict and somehow resolve it, potentially confusing the user. It's also unwise to fragment the conflict-resolving algorithm, allowing each client to implement it independently. -The server may compare request bodies, assuming that identical updates values mean retrying, but this assumption might be dangerously wrong (for example if the resource is a counter of some kind, then repeating identical requests are routine). -Adding the idempotency token (either directly as a random string, or indirectly in a form of drafts) solves this problem. +The server retrieves the actual resource revision and finds it to be 124. How should it respond correctly? Returning the 409 Conflict code will force the client to try to understand the nature of the conflict and somehow resolve it, potentially confusing the user. It is also unwise to fragment the conflict-resolving algorithm and allow each client to implement it independently. +The server can compare request bodies, assuming that identical requests mean retrying. However, this assumption might be dangerously wrong (for example if the resource is a counter of some kind, repeating identical requests is routine). +Adding the idempotency token (either directly as a random string or indirectly in the form of drafts) solves this problem. POST /resource/updates X-Idempotency-Token: <token> { @@ -2223,7 +2223,7 @@ X-Idempotency-Token: <token> } → 201 Created -— the server found out that the same token was used in creating revision 124, which means the client is retrying the request. +— the server determined that the same token was used in creating revision 124 indicating the client is retrying the request. Or: POST /resource/updates X-Idempotency-Token: <token> @@ -2233,22 +2233,22 @@ X-Idempotency-Token: <token> } → 409 Conflict -— 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 advanced optimizations possible. If the server detects an access conflict, it could try to resolve it, “rebasing” the update like modern version control systems do, and return a 200 OK instead of a 409 Conflict. This logic dramatically improves user experience, being fully backward-compatible, and helps to avoid conflict-resolving code fragmentation. -Also, be warned: clients are bad at implementing idempotency tokens. Two problems are common: +— the server determined that a different token was used in creating revision 124 indicating an access conflict. +Furthermore, adding idempotency tokens not only fixes the issue but also enables advanced optimizations. If the server detects an access conflict, it could attempt to resolve it by “rebasing” the update like modern version control systems do, and return a 200 OK instead of a 409 Conflict. This logic dramatically improves the user experience, being fully backward-compatible, and helps avoid code fragmentation for conflict resolution algorithms. +However, be warned: clients are bad at implementing idempotency tokens. Two common problems arise: -you can't really expect 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 a specific user and resource, not globally; -client developers might 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. +You can't really expect clients to generate truly random tokens. They might share the same seed or simply use weak algorithms or entropy sources. Therefore constraints must be placed on token checking, ensuring that tokens are unique to the specific user and resource rather than globally. +Client developers might misunderstand the concept and either generate new tokens for each repeated request (which degrades the UX but is otherwise harmless) or conversely use a single token in several requests (which is not harmless at all and could lead to catastrophic disasters; this is another reason to implement the suggestion in the previous clause). Writing an SDK and/or detailed documentation is highly recommended. 24. Don't Invent Security Practices -If the author of this book was given a dollar each time he had to implement the additional security protocol invented by someone, he would be already retired. The API developers' passion for signing request parameters or introducing complex schemes of exchanging passwords for tokens is as obvious as meaningless. -First, almost all security-enhancing procedures for every kind of operation are already invented. There is no need to re-think them anew; just take the existing approach and implement it. No self-invented algorithm for request signature checking provides the same level of preventing the Man-in-the-Middle attack as a TLS connection with mutual certificate pinning. -Second, it's quite presumptuous (and dangerous) to assume you're an expert in security. New attack vectors come every day, and being aware of all the actual threats is a full-day job. If you do something different during workdays, the security system designed by you will contain vulnerabilities that you have never heard about — for example, your password-checking algorithm might be susceptible to the timing attack, and your webserver, to the request splitting attack. -Just in case: any APIs must be provided over TLS 1.2 or higher (better 1.3). +If the author of this book were given a dollar each time he had to implement an additional security protocol invented by someone, he would be retired by now. API developers' inclination to create new signing procedures for requests or complex schemes of exchanging passwords for tokens is both obvious and meaningless. +First, there is no need to reinvent the wheel when it comes to security-enhancing procedures for various operations. All the algorithms you need are already invented, just adopt and implement them. No self-invented algorithm for request signature checking can provide the same level of protection against a Man-in-the-Middle attack as a TLS connection with mutual certificate pinning. +Second, assuming oneself to be an expert in security is presumptuous and dangerous. New attack vectors emerge daily, and staying fully aware of all actual threats is a full-time job. If you do something different during workdays, the security system you design will contain vulnerabilities that you have never heard about — for example, your password-checking algorithm might be susceptible to a timing attack or your webserver might be vulnerable to a request splitting attack. +Just in case: all APIs must be provided over TLS 1.2 or higher (preferably 1.3). 25. Help Partners With Security -It is equally important to provide such interfaces to partners that would minimize possible security problems for them. +It is equally important to provide interfaces to partners that minimize potential security problems for them. Bad: -// Allows partners for setting +// Allows partners to set // descriptions for their beverages PUT /v1/partner-api/{partner-id}⮠ /recipes/lungo/info @@ -2260,10 +2260,10 @@ GET /v1/partner-api/{partner-id}⮠ → "<script>alert(document.cookie)</script>" -Such an interface directly creates a stored XSS that potential attackers might exploit. While it's the responsibility of partners to sanitize inputs and safely display them, the big numbers are working against you: there always be inexperienced developers who are unaware of this vulnerability or haven't thought about it. In the worst case, this stored XSS might affect all the API consumers, not just a specific partner. -In these situations, first, we recommend sanitizing the data if it looks potentially exploitable (e.g., meant to be displayed in the UI and/or accessible by a direct link), and second, limit the blast radius so that stored exploits in one partner's data space can't affect other partners. If the functionality of unsafe data input is still required, the risks must be explicitly addressed: +Such an interface directly creates a stored XSS vulnerability that potential attackers might exploit. While it is the partners' responsibility to sanitize inputs and display them safely, the large numbers work against you: there will always be inexperienced developers who are unaware of this vulnerability or haven't considered it. In the worst case, this stored XSS might affect all API consumers, not just a specific partner. +In these situations, we recommend, first, sanitizing the data if it appears potentially exploitable (e.g. if it is meant to be displayed in the UI and/or is accessible through a direct link). Second, limiting the blast radius so that stored exploits in one partner's data space can't affect other partners. If the functionality of unsafe data input is still required, the risks must be explicitly addressed: Better (though not perfect): -// Allows for setting potentially +// Allows for setting a potentially // unsafe description for a beverage PUT /v1/partner-api/{partner-id}⮠ /recipes/lungo/info @@ -2278,7 +2278,7 @@ X-Dangerously-Allow-Raw-Value: true → "<script>alert(document.cookie)</script>" -One important finding is that if you allow executing scripts via API, always prefer typed input to unsafe input: +One important finding is that if you allow executing scripts via the API, always prefer typed input over unsafe input: Bad: POST /v1/run/sql { @@ -2304,23 +2304,23 @@ X-Dangerously-Allow-Raw-Value: true In the second case, you will be able to sanitize parameters and avoid SQL injections in a centralized manner. Let us remind the reader that sanitizing must be performed with state-of-the-art tools, not self-written regular expressions. 26. Use Globally Unique Identifiers It's considered good practice to use globally unique strings as entity identifiers, either semantic (e.g., "lungo" for beverage types) or random ones (e.g., UUID-4). It might turn out to be extremely useful if you need to merge data from several sources under a single identifier. -In general, we tend to advise using urn-like identifiers, e.g. urn:order:<uuid> (or just order:<uuid>). That helps a lot in dealing with legacy systems with different identifiers attached to the same entity. Namespaces in urns help to understand quickly which identifier is used and if there is a usage mistake. -One important implication: never use increasing numbers as external identifiers. Apart from the abovementioned reasons, it allows counting how many entities of each type there are in the system. Your competitors will be able to calculate a precise number of orders you have each day, for example. +In general, we tend to advise using URN-like identifiers, e.g. urn:order:<uuid> (or just order:<uuid>). That helps a lot in dealing with legacy systems with different identifiers attached to the same entity. Namespaces in URNs help to quickly understand which identifier is used and if there is a usage mistake. +One important implication: never use increasing numbers as external identifiers. Apart from the abovementioned reasons, it allows counting how many entities of each type there are in the system. Your competitors will be able to calculate the precise number of orders you have each day, for example. 27. Stipulate Future Restrictions -With the API popularity growth, it will inevitably become necessary to introduce technical means of preventing illicit API usage, such as displaying captchas, setting honeypots, raising the “too many requests” exceptions, installing anti-DDoS proxies, etc. All these things cannot be done if the corresponding errors and messages were not described in the docs from the very beginning. +With the growth of API popularity, it will inevitably become necessary to introduce technical means of preventing illicit API usage, such as displaying captchas, setting honeypots, raising “too many requests” exceptions, installing anti-DDoS proxies, etc. All these things cannot be done if the corresponding errors and messages were not described in the docs from the very beginning. You are not obliged to actually generate those exceptions, but you might stipulate this possibility in the docs. For example, you might describe the 429 Too Many Requests error or captcha redirect but implement the functionality when it's actually needed. -It is extremely important to leave room for multi-factored authentication (such as TOTP, SMS, or 3D-secure-like technologies) if it's possible to make payments through the API. In this case, it's a must-have from the very beginning. -NB: this rule has an important implication: always separate endpoints for different API families. (This may seem obvious, but many API developers fail to follow it.) If you provide a server-to-server API, a service for end users, and a widget to be embedded in third-party apps — all these APIs must be served from different endpoints to allow for different security measures (let's say, mandatory API keys, login requirement, and solving captcha respectively). +It is extremely important to leave room for multi-factor authentication (such as TOTP, SMS, or 3D-secure-like technologies) if it's possible to make payments through the API. In this case, it's a must-have from the very beginning. +NB: this rule has an important implication: always separate endpoints for different API families. (This may seem obvious, but many API developers fail to follow it.) If you provide a server-to-server API, a service for end users, and a widget to be embedded in third-party apps — all these APIs must be served from different endpoints to allow for different security measures (e.g., mandatory API keys, forced login, and solving captcha respectively). 28. No Bulk Access to Sensitive Data -If it's possible to access the API users' personal data, bank card numbers, private messages, or any other kind of information, exposing which might seriously harm users, partners, and/or the API vendor — there must be no methods for bulk retrieval of the data, or at least there must be rate limiters, page size restrictions, and, ideally, multi-factored authentication in front of them. +If it's possible to access the API users' personal data, bank card numbers, private messages, or any other kind of information that, if exposed, might seriously harm users, partners, and/or the API vendor, there must be no methods for bulk retrieval of the data, or at least there must be rate limiters, page size restrictions, and ideally, multi-factor authentication in front of them. Often, making such offloads on an ad-hoc basis, i.e., bypassing the API, is a reasonable practice. 29. 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. -It is important to understand that the user's language and the user's jurisdiction are different things. Your API working cycle must always store the user's location. It might be stated either explicitly (requests contain geographical coordinates) or implicitly (initial location-bound request initiates session creation which stores the location), but no correct localization is possible in absence of location data. In most cases reducing the location to just a country code is enough. -The thing is that lots of parameters that potentially affect data formats depend not on language but on the user's location. To name a few: number formatting (integer and fractional part delimiter, digit groups delimiter), date formatting, the first day of the week, keyboard layout, measurement units system (which might be non-decimal!), etc. In some situations, you need to store two locations: user residence location and user “viewport.” For example, if a US citizen is planning a European trip, it's convenient to show prices in local currency, but measure distances in miles and feet. +All endpoints must accept language parameters (e.g., in the form of the Accept-Language header), even if they are not currently being used. +It is important to understand that the user's language and the user's jurisdiction are different things. Your API working cycle must always store the user's location. It might be stated either explicitly (requests contain geographical coordinates) or implicitly (initial location-bound request initiates session creation which stores the location) — but no correct localization is possible in the absence of location data. In most cases reducing the location to just a country code is enough. +The thing is that lots of parameters that potentially affect data formats depend not on language but on the user's location. To name a few: number formatting (integer and fractional part delimiter, digit groups delimiter), date formatting, the first day of the week, keyboard layout, measurement units system (which might be non-decimal!), etc. In some situations, you need to store two locations: the user's residence location and the user's “viewport.” For example, if a US citizen is planning a European trip, it's convenient to show prices in the local currency but measure distances in miles and feet. Sometimes explicit location passing is not enough since there are lots of territorial conflicts in the world. How the API should behave when user coordinates lie within disputed regions is a legal matter, regretfully. The author of this book once had to implement a “state A territory according to state B official position” concept. -Important: mark a difference between localization for end users and localization for developers. In the examples above, the localized_message field is meant for the user; the app should show it if there is no specific handler for this error exists in the client code. This message must be written in the user's language and formatted according to the user's location. But the details.checks_failed[].message is meant to be read by developers examining the problem. So it must be written and formatted in a manner that suits developers best — which usually means “in English,” as English is a de-facto standard in software development. -Worth mentioning is that the localized_ prefix in the examples is used to differentiate messages to users from messages to developers. A concept like that must be, of course, explicitly stated in your API docs. +Important: mark a difference between localization for end users and localization for developers. In the examples above, the localized_message field is meant for the user; the app should show it if no specific handler for this error exists in the client code. This message must be written in the user's language and formatted according to the user's location. But the details.checks_failed[].message is meant to be read by developers examining the problem. So it must be written and formatted in a manner that suits developers best — which usually means “in English,” as English is a de facto standard in software development. +It is worth mentioning that the localized_ prefix in the examples is used to differentiate messages to users from messages to developers. A concept like that must be, of course, explicitly stated in your API docs. And one more thing: all strings must be UTF-8, no exclusions.Chapter 14. Annex to Section I. Generic API Example Let's summarize the current state of our API study. 1. Offer Search @@ -3500,6 +3500,7 @@ POST /v1/bulk-status-change If you can avoid creating such endpoints — do it. In server-to-server integrations, the profit is marginal. In modern networks that support QUIC and request multiplexing, it's also dubious. If you can not, make the endpoint atomic and provide SDKs to help partners avoid typical mistakes. If implementing an atomic endpoint is not possible, elaborate on the API design thoroughly, keeping in mind the caveats we discussed. +Whichever option you choose, it is crucially important to include a breakdown of the sub-requests in the response. For atomic endpoints, this entails ensuring that the error message contains a list of errors that prevented the request execution, ideally encompassing the potential errors as well (i.e., the results of validity checks for all the sub-requests). For non-atomic endpoints, it means returning a list of statuses corresponding to each sub-request along with errors that occurred during the execution. One of the approaches that helps minimize potential issues is developing a “mixed” endpoint, in which the operations that can affect each other are grouped: POST /v1/bulk-status-change @@ -3525,7 +3526,196 @@ POST /v1/bulk-status-change }] } -Let us also stress that nested operations (or sets of operations) must be idempotent per se. If they are not, you need to somehow deterministically generate internal idempotency tokens for each operation. The simplest approach is to consider the internal token equal to the external one if it is possible within the subject area. Otherwise, you will need to employ some constructed tokens — in our case, let's say, in the <order_id>:<external_token> form.Chapter 24. Partial UpdatesChapter 25. Degradation and PredictabilitySection III. The Backward CompatibilityChapter 26. The Backward Compatibility Problem Statement +Let us also stress that nested operations (or sets of operations) must be idempotent per se. If they are not, you need to somehow deterministically generate internal idempotency tokens for each operation. The simplest approach is to consider the internal token equal to the external one if it is possible within the subject area. Otherwise, you will need to employ some constructed tokens — in our case, let's say, in the <order_id>:<external_token> form.Chapter 24. Partial Updates +The case of partial application of the list of changes described in the previous chapter naturally leads us to the next typical API design problem. What if the operation involves a low-level overwriting of several data fields rather than an atomic idempotent procedure (as in the case of changing the order status)? Let's take a look at the following example: +// Creates an order +// consisting of two beverages +POST /v1/orders/ +X-Idempotency-Token: <token> +{ + "delivery_address", + "items": [{ + "recipe": "lungo" + }, { + "recipe": "latte", + "milk_type": "oat" + }] +} +→ +{ "order_id" } + +// Partially updates the order +// by changing the volume +// of the second beverage +PATCH /v1/orders/{id} +{ + "items": [ + // `null` indicates + // no changes for the + // first beverage + null, + // list of properties + // to change for + // the second beverage + {"volume": "800ml"} + ] +} +→ +{ /* Changes accepted */ } + +This signature is inherently flawed as its readability is dubious. What does the empty first element in the array mean, deletion of an element or absence of changes? What will happen with fields that are not passed (delivery_address, milk_type)? Will they reset to default values or remain unchanged? +The most notorious thing here is that no matter which option you choose, your problems have just begun. Let's say we agree that the "items":[null, {…}]} construct means the first array element remains untouched. So how do we delete it if needed? Do we invent another “nullish” value specifically to denote removal? The same issue applies to field values: if skipping a field in a request means it should remain unchanged, then how do we reset it to the default value? +Partially updating a resource is one of the most frequent tasks that API developers have to solve, and unfortunately, it is also one of the most complicated. Attempts to take shortcuts and simplify the implementation often lead to numerous problems in the future. +A trivial solution is to always overwrite the requested entity completely, which means requiring the passing of the entire object to fully replace the current state and return the new one. However, this simple solution is frequently dismissed due to several reasons: + +Increased request sizes and, consequently, higher traffic consumption +The necessity to detect which fields were actually changed in order to generate proper signals (events) for change listeners +The inability to facilitate collaborative editing of the object, meaning allowing two clients to edit different properties of the object in parallel as clients send the full object state as they know it and overwrite each other's changes as they are unaware of them. + +To avoid these issues, developers sometimes implement a naïve solution: + +Clients only pass the fields that have changed +To reset the values of certain fields and to delete or skip array elements some “special” values are used. + +A full example of an API implementing the naïve approach would look like this: +// Partially rewrites the order: +// * resets delivery address +// to the default values +// * leaves the first beverage +// intact +// * removes the second beverage +PATCH /v1/orders/{id} +{ + // “Special” value #1: + // reset the field + "delivery_address": null + "items": [ + // “Special” value #2: + // do nothing to the entity + {}, + // “Special” value #3: + // delete an entity + false + ] +} + +This solution allegedly solves the aforementioned problems: + +Traffic consumption is reduced as only the changed fields are transmitted, and unchanged entities are fully omitted (in our case, replaced with the special value {}). +Notifications regarding state changes will only be generated for the fields and entities passed in the request. +If two clients edit different fields, no access conflict is generated and both sets of changes are applied. + +However, upon closer examination all these conclusions seem less viable: + +We have already described the reasons for increased traffic consumption (excessive polling, lack of pagination and/or field size restrictions) in the “Describing Final Interfaces” chapter, and these issues have nothing to do with passing extra fields (and if they do, it implies that a separate endpoint for “heavy” data is needed). +The concept of passing only the fields that have actually changed shifts the burden of detecting which fields have changed onto the client developers' shoulders: + +Not only does the complexity of implementing the comparison algorithm remain unchanged but we also run the risk of having several independent realizations. +The capability of the client to calculate these diffs doesn't relieve the server developers of the duty to do the same as client developers might make mistakes or overlook certain aspects. + + +Finally, the naïve approach of organizing collaborative editing by allowing conflicting operations to be carried out if they don't touch the same fields works only if the changes are transitive. In our case, they are not: the result of simultaneously removing the first element in the list and editing the second one depends on the execution order. + +Often, developers try to reduce the outgoing traffic volume as well by returning an empty server response for modifying operations. Therefore, two clients editing the same entity do not see the changes made by each other until they explicitly refresh the state, which further increases the chance of yielding highly unexpected results. + + + +A more consistent solution is to split an endpoint into several idempotent sub-endpoints, each having its own independent identifier and/or address (which is usually enough to ensure the transitivity of the operations). This approach aligns well with the decomposition principle we discussed in the “Isolating Responsibility Areas” chapter. +// Creates an order +// comprising two beverages +POST /v1/orders/ +{ + "parameters": { + "delivery_address" + }, + "items": [{ + "recipe": "lungo" + }, { + "recipe": "latte", + "milk_type": "oats" + }] +} +→ +{ + "order_id", + "created_at", + "parameters": { + "delivery_address" + }, + "items": [ + { "item_id", "status"}, + { "item_id", "status"} + ] +} + +// Changes the parameters +// of the second order +PUT /v1/orders/{id}/parameters +{ "delivery_address" } +→ +{ "delivery_address" } + +// Partially changes the order +// by rewriting the parameters +// of the second beverage +PUT /v1/orders/{id}/items/{item_id} +{ + // All the fields are passed, + // even if only one has changed + "recipe", "volume", "milk_type" +} +→ +{ "recipe", "volume", "milk_type" } + +// Deletes one of the beverages +DELETE /v1/orders/{id}/items/{item_id} + +Now to reset the volume field it is enough not to pass it in the PUT items/{item_id}. Also note that the operations of removing one beverage and editing another one became transitive. +This approach also allows for separating read-only and calculated fields (such as created_at and status) from the editable ones without creating ambivalent situations (such as what should happen if the client tries to modify the created_at field). +Applying this pattern is typically sufficient for most APIs that manipulate composite entities. However, it comes with a price as it sets high standards for designing the decomposed interfaces (otherwise a once neat API will crumble with further API expansion) and the necessity to make many requests to replace a significant subset of the entity's fields (which implies exposing the functionality of applying bulk changes, the undesirability of which we discussed in the previous chapter). +NB: while decomposing endpoints, it's tempting to split editable and read-only data. Then the latter might be cached for a long time and there will be no need for sophisticated list iteration techniques. The plan looks great on paper; however, with API expansion, immutable data often ceases to be immutable which is only solvable by creating new versions of the interfaces. We recommend explicitly pronouncing some data non-modifiable in one of the following two cases: either (1) it really cannot become editable without breaking backward compatibility or (2) the reference to the resource (such as, let's say, a link to an image) is fetched via the API itself and you can make these links persistent (i.e., if the image is updated, a new link is generated instead of overwriting the content the old one points to). +Resolving Conflicts of Collaborative Editing +The idea of applying changes to a resource state through independent atomic idempotent operations looks attractive as a conflict resolution technique as well. As subcomponents of the resource are fully overwritten, it is guaranteed that the result of applying the changes will be exactly what the user saw on the screen of their device, even if they had observed an outdated version of the resource. However, this approach helps very little if we need a high granularity of data editing as it's implemented in modern services for collaborative document editing and version control systems (as we will need to implement endpoints with the same level of granularity, literally one for each symbol in the document). +To make true collaborative editing possible, a specifically designed format for describing changes needs to be implemented. It must allow for: + +ensuring the maximum granularity (each operation corresponds to one distinct user's action) +implementing conflict resolution policies. + +In our case, we might take this direction: +POST /v1/order/changes +X-Idempotency-Token: <token> +{ + // The revision the client + // observed when making + // the changes + "known_revision", + "changes": [{ + "type": "set", + "field": "delivery_address", + "value": <new value> + }, { + "type": "unset_item_field", + "item_id", + "field": "volume" + }], + … +} + +This approach is much more complex to implement, but it is the only viable technique for realizing collaborative editing as it explicitly reflects the exact actions the client applied to an entity. Having the changes in this format also allows for organizing offline editing with accumulating changes on the client side for the server to resolve the conflict later based on the revision history. +NB: one approach to this task is developing a set of operations in which all actions are transitive (i.e., the final state of the entity does not change regardless of the order in which the changes were applied). One example of such a nomenclature is CRDT. However, we consider this approach viable only in some subject areas, as in real life, non-transitive changes are always possible. If one user entered new text in the document and another user removed the document completely, there is no way to automatically resolve this conflict that would satisfy both users. The only correct way of resolving this conflict is explicitly asking users which option for mitigating the issue they prefer.Chapter 25. Degradation and Predictability +In the previous chapters, we repeatedly discussed that the background level of errors is not just unavoidable, but in many cases, APIs are deliberately designed to tolerate errors to make the system more scalable and predictable. +But let's ask ourselves a question: what does a “more predictable system” mean? For an API vendor, the answer is simple: the distribution and number of errors are both indicators of technical problems (if the numbers are growing unexpectedly) and KPIs for technical refactoring (if the numbers are decreasing after the release). +However, for partner developers, the concept of “API predictability” means something completely different: how solidly they can cover the API use cases (both happy and unhappy paths) in their code. In other words, how well one can understand based on the documentation and the nomenclature of API methods what errors might arise during the API work cycle and how to handle them. +Why is optimistic concurrency control better than acquiring locks from the partner's point of view? Because if the revision conflict error is received, it's obvious to a developer what to do about it: update the state and try again (the easiest approach is to show the new state to the end user and ask them what to do next). But if the developer can't acquire a lock in a reasonable time then… what useful action can they take? Retrying most certainly won't change anything. Show something to the user… but what exactly? An endless spinner? Ask the user to make a decision — give up or wait a bit longer? +While designing the API behavior, it's extremely important to imagine yourself in the partner developer's shoes and consider the code they must write to solve the arising issues (including timeouts and backend unavailability). This book comprises many specific tips on typical problems; however, you need to think about atypical ones on your own. +Here are some general pieces of advice that might come in handy: + +If you can include recommendations on resolving the error in the error response itself, do it unconditionally (but keep in mind there should be two sets of recommendations, one for the user who will see the message in the application and one for the developer who will find it in the logs) +If errors emitted by some endpoint are not critical for the main functionality of the integration, explicitly describe this fact in the documentation. Developers may not guess to wrap the corresponding code in a try-catch block. Providing code samples and guidance on what default value or behavior to use in case of an error is even better. +Remember that no matter how exquisite and comprehensive your error nomenclature is, a developer can always encounter a transport-level error or a network timeout, which means they need to restore the application state when the tips from the backend are not available. There should be an obvious default sequence of steps to handle unknown problems. +Finally, when introducing new types of errors, don't forget about old clients that are unaware of these new errors. The aforementioned “default reaction” to obscure issues should cover these new scenarios. + +In an ideal world, to help partners “degrade properly,” a meta-API should exist, allowing for determining the status of the endpoints of the main API. This way, partners would be able to automatically enable fallbacks if some functionality is unavailable. In the real world, alas, if a widespread outage occurs, APIs for checking the status of APIs are commonly unavailable as well.Section III. The Backward CompatibilityChapter 26. The Backward Compatibility Problem Statement As usual, let's conceptually define “backward compatibility” before we start. Backward compatibility is a feature of the entire API system to be stable in time. It means the following: the code that developers have written using your API continues working functionally correctly for a long period of time. There are two important questions to this definition and two explanations: diff --git a/docs/API.en.pdf b/docs/API.en.pdf index 8a83c07..f47f82f 100644 Binary files a/docs/API.en.pdf and b/docs/API.en.pdf differ diff --git a/docs/API.ru.epub b/docs/API.ru.epub index 896a55f..20df941 100644 Binary files a/docs/API.ru.epub and b/docs/API.ru.epub differ diff --git a/docs/API.ru.html b/docs/API.ru.html index 76a1e0c..3df29e4 100644 --- a/docs/API.ru.html +++ b/docs/API.ru.html @@ -577,7 +577,7 @@ ul.references li p a.back-anchor { Ссылка: - СодержаниеВведениеГлава 1. О структуре этой книгиГлава 2. Определение APIГлава 3. Обзор существующих решений в области разработки APIГлава 4. Критерии качества APIГлава 5. API-first подходГлава 6. Обратная совместимостьГлава 7. О версионированииГлава 8. Условные обозначения и терминологияРаздел I. Проектирование APIГлава 9. Пирамида контекстов APIГлава 10. Определение области примененияГлава 11. Разделение уровней абстракцииГлава 12. Разграничение областей ответственностиГлава 13. Описание конечных интерфейсовГлава 14. Приложение к разделу I. Модельный API[В разработке] Раздел II. Паттерны дизайна APIГлава 15. О паттернах проектирования в контексте APIГлава 16. Аутентификация партнёров и авторизация вызовов APIГлава 17. Стратегии синхронизацииГлава 18. Слабая консистентностьГлава 19. Асинхронность и управление временемГлава 20. Списки и организация доступа к нимГлава 21. Двунаправленные потоки данных. Push и poll-моделиГлава 22. Мультиплексирование сообщений. Асинхронная обработка событийГлава 23. Атомарность массовых измененийГлава 24. Частичные обновленияГлава 25. Деградация и предсказуемостьРаздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимостиГлава 27. О ватерлинии айсбергаГлава 28. Расширение через абстрагированиеГлава 29. Сильная связность и сопутствующие проблемыГлава 30. Слабая связностьГлава 31. Интерфейсы как универсальный паттернГлава 32. Блокнот душевного покоя[В разработке] Раздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологииГлава 34. Мифология RESTГлава 35. Составляющие HTTP запросов и их семантикаГлава 36. Преимущества и недостатки HTTP APIГлава 37. Принципы организации HTTP APIГлава 38. Работа с ошибками в HTTP APIГлава 39. Организация URL ресурсов и операций над ними в HTTP APIГлава 40. Заключительные положения и общие рекомендации[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт APIГлава 52. Бизнес-модели APIГлава 53. Формирование продуктового виденияГлава 54. Взаимодействие с разработчикамиГлава 55. Взаимодействие с бизнес-аудиториейГлава 56. Линейка сервисов APIГлава 57. Ключевые показатели эффективности APIГлава 58. Идентификация пользователей и борьба с фродомГлава 59. Технические способы борьбы с несанкционированным доступом к APIГлава 60. Поддержка пользователей APIГлава 61. ДокументацияГлава 62. Тестовая средаГлава 63. Управление ожиданиями + СодержаниеВведениеГлава 1. О структуре этой книгиГлава 2. Определение APIГлава 3. Обзор существующих решений в области разработки APIГлава 4. Критерии качества APIГлава 5. API-first подходГлава 6. Обратная совместимостьГлава 7. О версионированииГлава 8. Условные обозначения и терминологияРаздел I. Проектирование APIГлава 9. Пирамида контекстов APIГлава 10. Определение области примененияГлава 11. Разделение уровней абстракцииГлава 12. Разграничение областей ответственностиГлава 13. Описание конечных интерфейсовГлава 14. Приложение к разделу I. Модельный API[В разработке] Раздел II. Паттерны дизайна APIГлава 15. О паттернах проектирования в контексте APIГлава 16. Аутентификация партнёров и авторизация вызовов APIГлава 17. Стратегии синхронизацииГлава 18. Слабая консистентностьГлава 19. Асинхронность и управление временемГлава 20. Списки и организация доступа к нимГлава 21. Двунаправленные потоки данных. Push и poll-моделиГлава 22. Мультиплексирование сообщений. Асинхронная обработка событийГлава 23. Атомарность массовых измененийГлава 24. Частичные обновленияГлава 25. Деградация и предсказуемостьРаздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимостиГлава 27. О ватерлинии айсбергаГлава 28. Расширение через абстрагированиеГлава 29. Сильная связность и сопутствующие проблемыГлава 30. Слабая связностьГлава 31. Интерфейсы как универсальный паттернГлава 32. Блокнот душевного покоя[В разработке] Раздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологииГлава 34. Мифология RESTГлава 35. Составляющие HTTP запросов и их семантикаГлава 36. Преимущества и недостатки HTTP APIГлава 37. Принципы организации HTTP APIГлава 38. Работа с ошибками в HTTP APIГлава 39. Организация URL ресурсов и операций над ними в HTTP APIГлава 40. Заключительные положения и общие рекомендации[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт APIГлава 52. Бизнес-модели APIГлава 53. Формирование продуктового виденияГлава 54. Взаимодействие с разработчикамиГлава 55. Взаимодействие с бизнес-аудиториейГлава 56. Линейка сервисов APIГлава 57. Ключевые показатели эффективности APIГлава 58. Идентификация пользователей и борьба с фродомГлава 59. Технические способы борьбы с несанкционированным доступом к APIГлава 60. Поддержка пользователей APIГлава 61. ДокументацияГлава 62. Тестовая средаГлава 63. Управление ожиданиями @@ -598,7 +598,7 @@ ul.references li p a.back-anchor { Это произведение доступно по лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция — Некоммерческое использование») 4.0 Всемирная. Исходный код доступен на github.com/twirl/The-API-Book - Поделиться: facebook · twitter · linkedin · redditСодержаниеВведениеГлава 1. О структуре этой книгиГлава 2. Определение APIГлава 3. Обзор существующих решений в области разработки APIГлава 4. Критерии качества APIГлава 5. API-first подходГлава 6. Обратная совместимостьГлава 7. О версионированииГлава 8. Условные обозначения и терминологияРаздел I. Проектирование APIГлава 9. Пирамида контекстов APIГлава 10. Определение области примененияГлава 11. Разделение уровней абстракцииГлава 12. Разграничение областей ответственностиГлава 13. Описание конечных интерфейсовГлава 14. Приложение к разделу I. Модельный API[В разработке] Раздел II. Паттерны дизайна APIГлава 15. О паттернах проектирования в контексте APIГлава 16. Аутентификация партнёров и авторизация вызовов APIГлава 17. Стратегии синхронизацииГлава 18. Слабая консистентностьГлава 19. Асинхронность и управление временемГлава 20. Списки и организация доступа к нимГлава 21. Двунаправленные потоки данных. Push и poll-моделиГлава 22. Мультиплексирование сообщений. Асинхронная обработка событийГлава 23. Атомарность массовых измененийГлава 24. Частичные обновленияГлава 25. Деградация и предсказуемостьРаздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимостиГлава 27. О ватерлинии айсбергаГлава 28. Расширение через абстрагированиеГлава 29. Сильная связность и сопутствующие проблемыГлава 30. Слабая связностьГлава 31. Интерфейсы как универсальный паттернГлава 32. Блокнот душевного покоя[В разработке] Раздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологииГлава 34. Мифология RESTГлава 35. Составляющие HTTP запросов и их семантикаГлава 36. Преимущества и недостатки HTTP APIГлава 37. Принципы организации HTTP APIГлава 38. Работа с ошибками в HTTP APIГлава 39. Организация URL ресурсов и операций над ними в HTTP APIГлава 40. Заключительные положения и общие рекомендации[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт APIГлава 52. Бизнес-модели APIГлава 53. Формирование продуктового виденияГлава 54. Взаимодействие с разработчикамиГлава 55. Взаимодействие с бизнес-аудиториейГлава 56. Линейка сервисов APIГлава 57. Ключевые показатели эффективности APIГлава 58. Идентификация пользователей и борьба с фродомГлава 59. Технические способы борьбы с несанкционированным доступом к APIГлава 60. Поддержка пользователей APIГлава 61. ДокументацияГлава 62. Тестовая средаГлава 63. Управление ожиданиями + Поделиться: facebook · twitter · linkedin · redditСодержаниеВведениеГлава 1. О структуре этой книгиГлава 2. Определение APIГлава 3. Обзор существующих решений в области разработки APIГлава 4. Критерии качества APIГлава 5. API-first подходГлава 6. Обратная совместимостьГлава 7. О версионированииГлава 8. Условные обозначения и терминологияРаздел I. Проектирование APIГлава 9. Пирамида контекстов APIГлава 10. Определение области примененияГлава 11. Разделение уровней абстракцииГлава 12. Разграничение областей ответственностиГлава 13. Описание конечных интерфейсовГлава 14. Приложение к разделу I. Модельный API[В разработке] Раздел II. Паттерны дизайна APIГлава 15. О паттернах проектирования в контексте APIГлава 16. Аутентификация партнёров и авторизация вызовов APIГлава 17. Стратегии синхронизацииГлава 18. Слабая консистентностьГлава 19. Асинхронность и управление временемГлава 20. Списки и организация доступа к нимГлава 21. Двунаправленные потоки данных. Push и poll-моделиГлава 22. Мультиплексирование сообщений. Асинхронная обработка событийГлава 23. Атомарность массовых измененийГлава 24. Частичные обновленияГлава 25. Деградация и предсказуемостьРаздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимостиГлава 27. О ватерлинии айсбергаГлава 28. Расширение через абстрагированиеГлава 29. Сильная связность и сопутствующие проблемыГлава 30. Слабая связностьГлава 31. Интерфейсы как универсальный паттернГлава 32. Блокнот душевного покоя[В разработке] Раздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологииГлава 34. Мифология RESTГлава 35. Составляющие HTTP запросов и их семантикаГлава 36. Преимущества и недостатки HTTP APIГлава 37. Принципы организации HTTP APIГлава 38. Работа с ошибками в HTTP APIГлава 39. Организация URL ресурсов и операций над ними в HTTP APIГлава 40. Заключительные положения и общие рекомендации[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт APIГлава 52. Бизнес-модели APIГлава 53. Формирование продуктового виденияГлава 54. Взаимодействие с разработчикамиГлава 55. Взаимодействие с бизнес-аудиториейГлава 56. Линейка сервисов APIГлава 57. Ключевые показатели эффективности APIГлава 58. Идентификация пользователей и борьба с фродомГлава 59. Технические способы борьбы с несанкционированным доступом к APIГлава 60. Поддержка пользователей APIГлава 61. ДокументацияГлава 62. Тестовая средаГлава 63. Управление ожиданиями § @@ -2151,6 +2151,7 @@ GET /v1/price?recipe=lungo⮠ } } +NB: часто можно встретить подход, когда для неизменяемых данных выставляется очень длинный срок жизни кэша — год, а то и больше. С практической точки зрения это не имеет большого смысла (вряд ли можно всерьёз ожидать серьёзного снижения нагрузки на сервер по сравнению, скажем, с кэшированием на месяц), а вот цена ошибки существенно возрастает: если по какой-то причине будут закэшированы неверные данные (например, ошибка 404), эта проблема будет преследовать вас следующий год, а то и больше. Мы склонны рекомендовать выбирать разумные сроки кэширования в зависимости от того, насколько серьёзным окажется для бизнеса кэширование неверного значения. 22. Сохраняйте точность дробных чисел Там, где это позволено протоколом, дробные числа с фиксированной запятой — такие, как денежные суммы, например — должны передаваться в виде специально предназначенных для этого объектов, например, Decimal или аналогичных. Если в протоколе нет Decimal-типов (в частности, в JSON нет чисел с фиксированной запятой), следует либо привести к целому (путём домножения на указанный множитель), либо использовать строковый тип. @@ -3488,6 +3489,7 @@ POST /v1/bulk-status-change Если вы можете обойтись без таких эндпойнтов — обойдитесь. В server-to-server интеграциях экономия копеечная, в современных сетях с поддержкой протокола QUIC и мультиплексирования запросов тоже весьма сомнительная. Если такой эндпойнт всё же нужен, лучше реализовать его атомарно и предоставить SDK, которые помогут партнёрам не допускать типичные ошибки. Если реализовать атомарный эндпойнт невозможно, тщательно продумайте дизайн API, чтобы не допустить ошибок, подобных описанным выше. +Вне зависимости от выбранного подхода, ответы сервера должны включать разбивку по подзапросам. В случае атомарных эндпойнтов это означает включение в ответ списка ошибок, из-за которых исполнение запроса не удалось, в идеале — со всеми потенциальными ошибками (т.е. с результатами проверок каждого подзапроса на валидность). Для неатомарных эндпойнтов необходимо возвращать список со статусами каждого подзапроса и всеми возникшими ошибками. Один из подходов, позволяющих минимизировать возможные проблемы — разработать смешанный эндпойнт, в котором потенциально зависящие друг от друга операции группированы, например, вот так: POST /v1/bulk-status-change @@ -3511,7 +3513,191 @@ POST /v1/bulk-status-change }] } -На всякий случай уточним, что вложенные операции должны быть сами по себе идемпотентны. Если же это не так, то следует каким-то детерминированным образом сгенерировать внутренние ключи идемпотентности на каждую вложенную операцию в отдельности (в простейшем случае — считать токен идемпотентности внутренних запросов равным токену идемпотентости внешнего запроса, если это допустимо в рамках предметной области; иначе придётся использовать составные токены — в нашем случае, например, в виде <order_id>:<external_token>).Глава 24. Частичные обновленияГлава 25. Деградация и предсказуемостьРаздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимости +На всякий случай уточним, что вложенные операции должны быть сами по себе идемпотентны. Если же это не так, то следует каким-то детерминированным образом сгенерировать внутренние ключи идемпотентности на каждую вложенную операцию в отдельности (в простейшем случае — считать токен идемпотентности внутренних запросов равным токену идемпотентости внешнего запроса, если это допустимо в рамках предметной области; иначе придётся использовать составные токены — в нашем случае, например, в виде <order_id>:<external_token>).Глава 24. Частичные обновления +Описанный в предыдущей главе пример со списком операций, который может быть выполнен частично, естественным образом подводит нас к следующей проблеме дизайна API. Что, если изменение не является атомарной идемпотентной операцией (как изменение статуса заказа), а представляет собой низкоуровневую перезапись нескольких полей объекта? Рассмотрим следующий пример. +// Создаёт заказ из двух напитков +POST /v1/orders/ +X-Idempotency-Token: <токен> +{ + "delivery_address", + "items": [{ + "recipe": "lungo" + }, { + "recipe": "latte", + "milk_type": "oat" + }] +} +→ +{ "order_id" } + +// Частично перезаписывает заказ, +// обновляет объём второго напитка +PATCH /v1/orders/{id} +{ + "items": [ + // `null` показывает, что + // параметры первого напитка + // менять не надо + null, + // список изменений свойств + // второго напитка + {"volume": "800ml"} + ] +} +→ +{ /* изменения приняты */ } + +Эта сигнатура плоха сама по себе, поскольку её читабельность сомнительна. Что обозначает пустой первый элемент массива — это удаление элемента или указание на отсутствие изменений? Что произойдёт с полями, которые не указаны в операции обновления (delivery_address, milk_type) — они будут сброшены в значения по умолчанию или останутся неизменными? +Самое неприятное здесь — какой бы вариант вы ни выбрали, это только начало проблем. Допустим, мы договорились, что конструкция {"items":[null, {…}]} означает, что с первым элементом массива ничего не происходит, он не меняется. А как тогда всё-таки его удалить? Придумать ещё одно «зануляемое» значение специально для удаления? Аналогично, если значения неуказанных полей остаются без изменений — как сбросить их в значения по умолчанию? +Частичные изменения состояния ресурсов — одна из самых частых задач, которые решает разработчик API, и, увы, одна из самых сложных. Попытки обойтись малой кровью и упростить имплементацию зачастую приводят к очень большим проблемам в будущем. +Простое решение состоит в том, чтобы всегда перезаписывать объект целиком, т.е. требовать передачи полного объекта, полностью заменять им текущее состояние и возвращать в ответ на операцию новое состояние целиком. Однако это простое решение часто не принимается по нескольким причинам: + +повышенные размеры запросов и, как следствие, расход трафика; +необходимость вычислять, какие конкретно поля изменились — в частности для того, чтобы правильно сгенерировать сигналы (события) для подписчиков на изменения; +невозможность совместного доступа к объекту, когда два клиента независимо редактируют его свойства, поскольку клиенты всегда посылают полное состояние объекта, известное им, и переписывают изменения друг друга, поскольку о них не знают. + +Во избежание перечисленных проблем разработчики, как правило, реализуют некоторое наивное решение: + +клиент передаёт только те поля, которые изменились; +для сброса значения поля в значение по умолчанию или пропуска/удаления элементов массивов используются специально оговоренные значения. + +Если обратиться к примеру выше, наивный подход выглядит примерно так: +// Частично перезаписывает заказ: +// * сбрасывает адрес доставки +// в значение по умолчанию +// * не изменяет первый напиток +// * удаляет второй напиток +PATCH /v1/orders/{id} +{ + // Специальное значение №1: + // обнулить поле + "delivery_address": null + "items": [ + // Специальное значение №2: + // не выполнять никаких + // операций с объектом + {}, + // Специальное значение №3: + // удалить объект + false + ] +} + +Предполагается, что: + +повышенного расхода трафика можно избежать, передавая только изменившиеся поля и заменяя пропускаемые элементы специальными значениями ({} в нашем случае); +события изменения значения поля также будут генерироваться только по тем полям и объектам, которые переданы в запросе; +если два клиента делают одновременный запрос, но изменяют различные поля, конфликта доступа не происходит, и оба изменения применяются. + +Все эти соображения, однако, на поверку оказываются мнимыми: + +причины увеличенного расхода трафика (слишком частый поллинг, отсутствие пагинации и/или ограничений на размеры полей) мы разбирали в главе «Описание конечных интерфейсов», и передача лишних полей к ним не относится (а если и относится, то это повод декомпозировать эндпойнт); +концепция передачи только изменившихся полей по факту перекладывает ответственность определения, какие поля изменились, на клиент; + +это не только не снижает сложность имплементации этого кода, но и чревато его фрагментацией на несколько независимых клиентских реализаций; +существование клиентского алгоритма построения diff-ов не отменяет обязанность сервера уметь делать то же самое — поскольку клиентские разработчики могли ошибиться или просто полениться правильно вычислить изменившиеся поля; + + +наконец, подобная наивная концепция организации совместного доступа работает ровно до того момента, пока изменения транзитивны, т.е. результат не зависит от порядка выполнения операций (в нашим примере это уже не так — операции удаления первого элемента и редактирования первого элемента нетранзитивны); + +кроме того, часто в рамках той же концепции экономят и на исходящем трафике, возвращая пустой ответ сервера для модифицирующих операций; таким образом, два клиента, редактирующих одну и ту же сущность, не видят изменения друг друга, что ещё больше повышает вероятность получить совершенно неожиданные результаты. + + + +Более консистентное решение: разделить эндпойнт на несколько идемпотентных суб-эндпойнтов, имеющих независимые идентификаторы и/или адреса (чего обычно достаточно для обеспечения транзитивности операций). Этот подход также хорошо согласуется с принципом декомпозиции, который мы рассматривали в предыдущем главе «Разграничение областей ответственности». +// Создаёт заказ из двух напитков +POST /v1/orders/ +{ + "parameters": { + "delivery_address" + }, + "items": [{ + "recipe": "lungo" + }, { + "recipe": "latte", + "milk_type": "oats" + }] +} +→ +{ + "order_id", + "created_at", + "parameters": { + "delivery_address" + }, + "items": [ + { "item_id", "status"}, + { "item_id", "status"} + ] +} + +// Изменяет параметры, +// относящиеся ко всему заказу +PUT /v1/orders/{id}/parameters +{ "delivery_address" } +→ +{ "delivery_address" } + +// Частично перезаписывает заказ +// обновляет объём одного напитка +PUT /v1/orders/{id}/items/{item_id} +{ + // Все поля передаются, даже если + // изменилось только какое-то одно + "recipe", "volume", "milk_type" +} +→ +{ "recipe", "volume", "milk_type" } + +// Удаляет один из напитков в заказе +DELETE /v1/orders/{id}/items/{item_id} + +Теперь для удаления volume достаточно не передавать его в PUT items/{item_id}. Кроме того, обратите внимание, что операции удаления одного напитка и модификации другого теперь стали транзитивными. +Этот подход также позволяет отделить неизменяемые и вычисляемые поля (created_at и status) от изменяемых, не создавая двусмысленных ситуаций (что произойдёт, если клиент попытается изменить created_at?). +Применения этого паттерна, как правило, достаточно для большинства API, манипулирующих сложносоставленными сущностями, однако и недостатки у него тоже есть: высокие требования к качеству проектирования декомпозиции (иначе велик шанс, что стройное API развалится при дальнейшем расширении функциональности) и необходимость совершать множество запросов для изменения всей сущности целиком (из чего вытекает необходимость создания функциональности для внесения массовых изменений, нежелательность которой мы обсуждали в предыдущей главе). +NB: при декомпозиции эндпойнтов велик соблазн провести границу так, чтобы разделить изменяемые и неизменяемые данные. Тогда последние можно объявить кэшируемыми условно вечно и вообще не думать над проблемами пагинации и формата обновления. На бумаге план выглядит отлично, однако с ростом API неизменяемые данные частенько перестают быть таковыми, и тогда потребуется выпускать новые интерфейсы работы с данными. Мы скорее рекомендуем объявлять данные иммутабельными в одном из двух случаев: либо (1) они действительно не могут стать изменяемыми без слома обратной совместимости, либо (2) ссылка на ресурс (например, на изображение) поступает через API же, и вы обладаете возможностью сделать эти ссылки персистентными (т.е. при необходимости обновить изображение будете генерировать новую ссылку, а не перезаписывать контент по старой ссылке). +Разрешение конфликтов совместного редактирования +Идеи организации изменения состояния ресурса через независимые атомарные идемпотентные операции выглядит достаточно привлекательно и с точки зрения разрешения конфликтов доступа. Так как составляющие ресурса перезаписываются целиком, результатом записи будет именно то, что пользователь видел своими глазами на экране своего устройства, даже если при этом он смотрел на неактуальную версию. Однако этот подход очень мало помогает нам, если мы действительно обеспечить максимально гранулярное изменение данных, как, например, это сделано в онлайн-сервисах совместной работы с документами или системах контроля версий (поскольку для этого нам придётся сделать столь же гранулярные эндпойнты, т.е. буквально адресовать каждый символ документа по отдельности). +Для «настоящего» совместного редактирования необходимо будет разработать отдельный формат описания изменений, который позволит: + +иметь максимально возможную гранулярность (т.е. одна операция соответствует одному действию клиента); +реализовать политику разрешения конфликтов. + +В нашем случае мы можем пойти, например, вот таким путём: +POST /v1/order/changes +X-Idempotency-Token: <токен> +{ + // Какую ревизию ресурса + // видел пользователь, когда + // выполнял изменения + "known_revision", + "changes": [{ + "type": "set", + "field": "delivery_address", + "value": <новое значение> + }, { + "type": "unset_item_field", + "item_id", + "field": "volume" + }], + … +} + +Этот подход существенно сложнее в имплементации, но является единственным возможным вариантом реализации совместного редактирования, поскольку он явно отражает, что в действительности делал пользовать с представлением объекта. Имея данные в таком формате возможно организовать и оффлайн-редактирование, когда пользовательские изменения накапливаются и сервер впоследствии автоматически разрешает возможные конфликты, основываясь на истории ревизий. +NB: один из подходов к этой задаче — разработка такой номенклатуры операций над данными (например, CRDT), в которой любые действия транзитивны (т.е. конечное состояние системы не зависит от того, в каком порядке они были применены). Мы, однако, склонны считать такой подход применимым только к весьма ограниченным предметным областям — поскольку в реальной жизни нетранзитивные действия находятся почти всегда. Если один пользователь ввёл в документ новый текст, а другой пользователь удалил документ — никакого разумного (т.е. удовлетворительного с точки зрения обоих акторов) способа автоматического разрешения конфликта здесь нет, необходимо явно спросить пользователей, что бы они хотели сделать с возникшим конфликтом.Глава 25. Деградация и предсказуемость +В предыдущих главах мы много говорили о том, что фон ошибок — не только неизбежное зло в любой достаточно большой системе, но и, зачастую, осознанное решение, которое позволяет сделать систему более масштабируемой и предсказуемой. +Зададим себе, однако, вопрос: а что значит «более предсказуемая» система? Для нас как для вендора API это достаточно просто: процент ошибок (в разбивке по типам) достаточно стабилен, и им можно пользоваться как индикатором возникающих технических проблем (если он растёт) и как KPI для технических улучшений и рефакторингов (если он падает). +Но вот для разработчиков-партнёров понятие «предсказуемость поведения API» имеет совершенно другой смысл: насколько хорошо и полно они в своём коде могут покрыть различные сценарии использования API и потенциальные проблемы — или, иными словами, насколько явно из документации и номенклатуры методов и ошибок API становится ясно, какие типовые ошибки могут возникнуть и как с ними работать. +Чем, например, оптимистичное управление параллелизмом (см. главу «Стратегии синхронизации») лучше блокировок с точки зрения партнёра? Тем, что, получив ошибку несовпадения ревизий, разработчик понимает, какой код он должен написать: обновить состояние и попробовать ещё раз (в простейшем случае — показав новое состояние пользователю и предложив ему решить, что делать дальше). Если же разработчик пытается захватить lock и не может сделать этого в течение разумного времени, то… а что он может полезного сделать? Попробовать ещё раз — но результат ведь, скорее всего, не изменится. Показать пользователю… что? Бесконечный спиннер? Попросить пользователя принять какое решение — сдаться или ещё подождать? +При проектировании поведения вашего API исключительно важно представить себя на месте разработчика и попытаться понять, какой код он должен написать для разрешения возникающих ситуаций (включая сетевые таймауты и/или частичную недоступность вашего бэкенда). В этой книге приведено множество частных советов, как поступать в той или иной ситуации, но они, конечно, покрывают только типовые сценарии. О нетиповых вам придётся подумать самостоятельно. +Несколько общих советов, которые могут вам пригодиться: + +если вы можете включить в саму ошибку рекомендации, как с ней бороться — сделайте это не раздумывая (имейте в виду, что таких рекомендаций должно быть две — для пользователя, который увидит ошибку в приложении, и для разработчика, который будет разбирать логи); +если ошибки в каком-то эндпойнте некритичны для основной функциональности интеграции, очень желательно описать этот факт в документации (потому что разработчик может просто не догадаться обернуть соответствующий вызов в try-catch), а лучше — привести примеры, каким значением/поведением по умолчанию следует воспользоваться в случае получения ошибки; +не забывайте, что, какую бы стройную и всеобъемлющую систему ошибок вы ни выстроили, почти в любой среде разработчик может получить ошибку транспортного уровня или таймаут выполнения, а, значит, оказаться в ситуации, когда восстанавливать состояние надо, а «подсказки» от бэкенда недоступны; должна существовать какая-то достаточно очевидная последовательность действий «по умолчанию» для восстановления работы интеграции из любой точки; +наконец, при введении новых ошибок не забывайте о старых клиентах, которые про эти новые типы ошибок не знают; «реакция по умолчанию» на неизвестные ошибки должна в том числе покрывать и эти новые сценарии. + +В идеальном мире для «правильной деградации» клиентов желательно иметь мета-API, позволяющее определить статус доступности для эндпойнтов основного API — тогда партнёры смогут, например, автоматически включать fallback-и, если какая-то функциональность не работает. (В реальном мире, увы, если на уровне сервиса наблюдаются масштабные проблемы, то обычно и API статуса доступности оказывается ими затронуто.)Раздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимости Как обычно, дадим смысловое определение «обратной совместимости», прежде чем начинать изложение. Обратная совместимость — это свойство всей системы API быть стабильной во времени. Это значит следующее: код, написанный разработчиками с использованием вашего API, продолжает работать функционально корректно в течение длительного времени. К этому определению есть два больших вопроса, и два уточнения к ним. diff --git a/docs/API.ru.pdf b/docs/API.ru.pdf index 264b5c8..3a57f3d 100644 Binary files a/docs/API.ru.pdf and b/docs/API.ru.pdf differ diff --git a/docs/index.html b/docs/index.html index 1350829..7e264b2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -90,8 +90,8 @@ Chapter 21. Bidirectional Data Flows. Push and Poll Models Chapter 22. Multiplexing Notifications. Asynchronous Event Processing Chapter 23. Atomicity of Bulk Changes -Chapter 24. Partial Updates -Chapter 25. Degradation and Predictability +Chapter 24. Partial Updates +Chapter 25. Degradation and Predictability diff --git a/docs/index.ru.html b/docs/index.ru.html index ae14312..7729b92 100644 --- a/docs/index.ru.html +++ b/docs/index.ru.html @@ -90,8 +90,8 @@ Глава 21. Двунаправленные потоки данных. Push и poll-модели Глава 22. Мультиплексирование сообщений. Асинхронная обработка событий Глава 23. Атомарность массовых изменений -Глава 24. Частичные обновления -Глава 25. Деградация и предсказуемость +Глава 24. Частичные обновления +Глава 25. Деградация и предсказуемость
@@ -598,7 +598,7 @@ ul.references li p a.back-anchor { Это произведение доступно по лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция — Некоммерческое использование») 4.0 Всемирная. Исходный код доступен на github.com/twirl/The-API-Book - Поделиться: facebook · twitter · linkedin · redditСодержаниеВведениеГлава 1. О структуре этой книгиГлава 2. Определение APIГлава 3. Обзор существующих решений в области разработки APIГлава 4. Критерии качества APIГлава 5. API-first подходГлава 6. Обратная совместимостьГлава 7. О версионированииГлава 8. Условные обозначения и терминологияРаздел I. Проектирование APIГлава 9. Пирамида контекстов APIГлава 10. Определение области примененияГлава 11. Разделение уровней абстракцииГлава 12. Разграничение областей ответственностиГлава 13. Описание конечных интерфейсовГлава 14. Приложение к разделу I. Модельный API[В разработке] Раздел II. Паттерны дизайна APIГлава 15. О паттернах проектирования в контексте APIГлава 16. Аутентификация партнёров и авторизация вызовов APIГлава 17. Стратегии синхронизацииГлава 18. Слабая консистентностьГлава 19. Асинхронность и управление временемГлава 20. Списки и организация доступа к нимГлава 21. Двунаправленные потоки данных. Push и poll-моделиГлава 22. Мультиплексирование сообщений. Асинхронная обработка событийГлава 23. Атомарность массовых измененийГлава 24. Частичные обновленияГлава 25. Деградация и предсказуемостьРаздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимостиГлава 27. О ватерлинии айсбергаГлава 28. Расширение через абстрагированиеГлава 29. Сильная связность и сопутствующие проблемыГлава 30. Слабая связностьГлава 31. Интерфейсы как универсальный паттернГлава 32. Блокнот душевного покоя[В разработке] Раздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологииГлава 34. Мифология RESTГлава 35. Составляющие HTTP запросов и их семантикаГлава 36. Преимущества и недостатки HTTP APIГлава 37. Принципы организации HTTP APIГлава 38. Работа с ошибками в HTTP APIГлава 39. Организация URL ресурсов и операций над ними в HTTP APIГлава 40. Заключительные положения и общие рекомендации[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт APIГлава 52. Бизнес-модели APIГлава 53. Формирование продуктового виденияГлава 54. Взаимодействие с разработчикамиГлава 55. Взаимодействие с бизнес-аудиториейГлава 56. Линейка сервисов APIГлава 57. Ключевые показатели эффективности APIГлава 58. Идентификация пользователей и борьба с фродомГлава 59. Технические способы борьбы с несанкционированным доступом к APIГлава 60. Поддержка пользователей APIГлава 61. ДокументацияГлава 62. Тестовая средаГлава 63. Управление ожиданиями + Поделиться: facebook · twitter · linkedin · redditСодержаниеВведениеГлава 1. О структуре этой книгиГлава 2. Определение APIГлава 3. Обзор существующих решений в области разработки APIГлава 4. Критерии качества APIГлава 5. API-first подходГлава 6. Обратная совместимостьГлава 7. О версионированииГлава 8. Условные обозначения и терминологияРаздел I. Проектирование APIГлава 9. Пирамида контекстов APIГлава 10. Определение области примененияГлава 11. Разделение уровней абстракцииГлава 12. Разграничение областей ответственностиГлава 13. Описание конечных интерфейсовГлава 14. Приложение к разделу I. Модельный API[В разработке] Раздел II. Паттерны дизайна APIГлава 15. О паттернах проектирования в контексте APIГлава 16. Аутентификация партнёров и авторизация вызовов APIГлава 17. Стратегии синхронизацииГлава 18. Слабая консистентностьГлава 19. Асинхронность и управление временемГлава 20. Списки и организация доступа к нимГлава 21. Двунаправленные потоки данных. Push и poll-моделиГлава 22. Мультиплексирование сообщений. Асинхронная обработка событийГлава 23. Атомарность массовых измененийГлава 24. Частичные обновленияГлава 25. Деградация и предсказуемостьРаздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимостиГлава 27. О ватерлинии айсбергаГлава 28. Расширение через абстрагированиеГлава 29. Сильная связность и сопутствующие проблемыГлава 30. Слабая связностьГлава 31. Интерфейсы как универсальный паттернГлава 32. Блокнот душевного покоя[В разработке] Раздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологииГлава 34. Мифология RESTГлава 35. Составляющие HTTP запросов и их семантикаГлава 36. Преимущества и недостатки HTTP APIГлава 37. Принципы организации HTTP APIГлава 38. Работа с ошибками в HTTP APIГлава 39. Организация URL ресурсов и операций над ними в HTTP APIГлава 40. Заключительные положения и общие рекомендации[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт APIГлава 52. Бизнес-модели APIГлава 53. Формирование продуктового виденияГлава 54. Взаимодействие с разработчикамиГлава 55. Взаимодействие с бизнес-аудиториейГлава 56. Линейка сервисов APIГлава 57. Ключевые показатели эффективности APIГлава 58. Идентификация пользователей и борьба с фродомГлава 59. Технические способы борьбы с несанкционированным доступом к APIГлава 60. Поддержка пользователей APIГлава 61. ДокументацияГлава 62. Тестовая средаГлава 63. Управление ожиданиями § @@ -2151,6 +2151,7 @@ GET /v1/price?recipe=lungo⮠ } }