mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-04-17 11:06:25 +02:00
Builder upgraded
This commit is contained in:
parent
db4343550f
commit
33abe495e6
BIN
docs/API.en.epub
BIN
docs/API.en.epub
Binary file not shown.
120
docs/API.en.html
120
docs/API.en.html
@ -709,7 +709,7 @@ ul.references li p a.back-anchor {
|
||||
</ol>
|
||||
<p>The sentences “a major API version” and “a new API version, containing backward-incompatible changes” are considered equivalent.</p>
|
||||
<p>It is usually (though not necessary) agreed that the last stable API release might be referenced by either a full version (e.g., <code>1.2.3</code>) or a reduced one (<code>1.2</code> or just <code>1</code>). Some systems support more sophisticated schemes for defining the desired version (for example, <code>^1.2.3</code> reads like “get the last stable API release that is backward-compatible to the <code>1.2.3</code> version”) or additional shortcuts (for example, <code>1.2-beta</code> to refer to the last beta release of the <code>1.2</code> API version family). In this book, we will mostly use designations like <code>v1</code> (<code>v2</code>, <code>v3</code>, etc.) to denote the latest stable release of the <code>1.x.x</code> version family of an API.</p>
|
||||
<p>The practical meaning of this versioning system and the applicable policies will be discussed in more detail in the <a href="#back-compat-statement">“Backward Compatibility Problem Statement”</a> chapter.</p><div class="page-break"></div><h3><a href="#intro-terms-notation" class="anchor" id="intro-terms-notation">Chapter 8. Terms and Notation Keys</a><a href="#chapter-8" class="secondary-anchor" id="chapter-8"> </a></h3>
|
||||
<p>The practical meaning of this versioning system and the applicable policies will be discussed in more detail in the “<a href="#back-compat-statement">Backward Compatibility Problem Statement</a>” chapter.</p><div class="page-break"></div><h3><a href="#intro-terms-notation" class="anchor" id="intro-terms-notation">Chapter 8. Terms and Notation Keys</a><a href="#chapter-8" class="secondary-anchor" id="chapter-8"> </a></h3>
|
||||
<p>Software development is characterized, among other things, by the existence of many different engineering paradigms, whose adherents are sometimes quite aggressive towards other paradigms' adherents. While writing this book, we are deliberately avoiding using terms like “method,” “object,” “function,” and so on, using the neutral term “entity” instead. “Entity” means some atomic functionality unit, like a class, method, object, monad, prototype (underline what you think is right).</p>
|
||||
<p>As for an entity's components, we regretfully failed to find a proper term, so we will use the words “fields” and “methods.”</p>
|
||||
<p>Most of the examples of APIs will be provided in the form of JSON-over-HTTP endpoints. This is some sort of notation that, as we see it, helps to describe concepts in the most comprehensible manner. A <code>GET /v1/orders</code> endpoint call could easily be replaced with an <code>orders.get()</code> method call, local or remote; JSON could easily be replaced with any other data format. The semantics of statements shouldn't change.</p>
|
||||
@ -1583,11 +1583,11 @@ For example, the invalid price error is resolvable: a client could obtain a new
|
||||
<p>One of the most important tasks for an API developer is to make code that other developers write using the API easily readable and maintainable. Remember that the big numbers law always works against you: if some concept or call signature might be understood wrongly, they will be wrongly understood by the increasing number of partners as the API popularity grows.</p>
|
||||
<p><strong>NB</strong>: the examples in this chapter are meant to illustrate the consistency and readability problems that arise during API development. We're not giving specific advice regarding designing REST API (such advice will be given in the corresponding section of this book) or programming languages standard libraries; it's about the idea, not specific syntax.</p>
|
||||
<p>An important assertion at number 0:</p>
|
||||
<h5><a href="#chapter-13-paragraph-0" id="chapter-13-paragraph-0" class="anchor">0. Rules Must Not Be Applied Unthinkingly</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-1" id="chapter-13-paragraph-1" class="anchor">0. Rules Must Not Be Applied Unthinkingly</a></h5>
|
||||
<p>Rules are just simply formulated generalizations from one's experience. They are not to be applied unconditionally, and they don't make thinking redundant. Every rule has a rational reason to exist. If your situation doesn't justify following the rule — then you shouldn't do it.</p>
|
||||
<p>This idea applies to every concept listed below. If you get an unusable, bulky, unobvious API because you follow the rules, it's a motivation to revise the rules (or the API).</p>
|
||||
<p>It is important to understand that you can always introduce concepts of your own. For example, some frameworks willfully reject paired <code>set_entity</code> / <code>get_entity</code> methods in a favor of a single <code>entity()</code> method, with an optional argument. The crucial part is being systematic in applying the concept. If it's rendered into life, you must apply it to every single API method, or at the very least elaborate a naming rule to discern such polymorphic methods from regular ones.</p>
|
||||
<h5><a href="#chapter-13-paragraph-1" id="chapter-13-paragraph-1" class="anchor">1. Explicit Is Always Better Than Implicit</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-2" id="chapter-13-paragraph-2" class="anchor">2. Explicit Is Always Better Than Implicit</a></h5>
|
||||
<p>Entity name must explicitly tell what the entity does and what side effects to expect while using it.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>// Cancels an order
|
||||
@ -1617,7 +1617,7 @@ orders.calculateAggregatedStats({
|
||||
<p>Two important implications:</p>
|
||||
<p><strong>1.1.</strong> If the operation is modifying, it must be obvious from the signature. In particular, there might be no modifying operations named <code>getSomething</code> or using the <code>GET</code> HTTP verb.</p>
|
||||
<p><strong>1.2.</strong> If your API's nomenclature contains both synchronous and asynchronous operations, then (a)synchronicity must be apparent from signatures, <strong>or</strong> a naming convention must exist.</p>
|
||||
<h5><a href="#chapter-13-paragraph-2" id="chapter-13-paragraph-2" class="anchor">2. Specify Which Standards Are Used</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-3" id="chapter-13-paragraph-3" class="anchor">3. Specify Which Standards Are Used</a></h5>
|
||||
<p>Regretfully, humanity is unable to agree on the most trivial things, like which day starts the week, to say nothing about more sophisticated standards.</p>
|
||||
<p>So <em>always</em> specify exactly which standard is applied. Exceptions are possible if you're 100% sure that only one standard for this entity exists in the world, and every person on Earth is totally aware of it.</p>
|
||||
<p><strong>Bad</strong>: <code>"date": "11/12/2020"</code> — there are tons of date formatting standards; you can't even tell which number means the day number and which number means the month.</p>
|
||||
@ -1633,11 +1633,11 @@ or<br>
|
||||
<code>"duration": {"unit": "ms", "value": 5000}</code>.</p>
|
||||
<p>One particular implication of this rule is that money sums must <em>always</em> be accompanied by a currency code.</p>
|
||||
<p>It is also worth saying that in some areas the situation with standards is so spoiled that, whatever you do, someone got upset. A “classical” example is geographical coordinates order (latitude-longitude vs longitude-latitude). Alas, the only working method of fighting frustration there is the Serenity Notepad to be discussed in <a href="#back-compat-serenity-notepad">the corresponding chapter</a>.</p>
|
||||
<h5><a href="#chapter-13-paragraph-3" id="chapter-13-paragraph-3" class="anchor">3. Entities Must Have Concrete Names</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-4" id="chapter-13-paragraph-4" class="anchor">4. Entities Must Have Concrete Names</a></h5>
|
||||
<p>Avoid single amoeba-like words, such as “get,” “apply,” “make,” etc.</p>
|
||||
<p><strong>Bad</strong>: <code>user.get()</code> — hard to guess what is actually returned.</p>
|
||||
<p><strong>Better</strong>: <code>user.get_id()</code>.</p>
|
||||
<h5><a href="#chapter-13-paragraph-4" id="chapter-13-paragraph-4" class="anchor">4. Don't Spare the Letters</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-5" id="chapter-13-paragraph-5" class="anchor">5. Don't Spare the Letters</a></h5>
|
||||
<p>In the 21st century, there's no need to shorten entities' names.</p>
|
||||
<p><strong>Bad</strong>: <code>order.getTime()</code> — unclear, what time is actually returned: order creation time, order preparation time, order waiting time?…</p>
|
||||
<p><strong>Better</strong>: <code>order.getEstimatedDeliveryTime()</code>.</p>
|
||||
@ -1656,7 +1656,7 @@ strpbrk (str1, str2)
|
||||
</code></pre>
|
||||
<p>— though it's highly disputable whether this function should exist at all; a feature-rich search function would be much more convenient. Also, shortening a <code>string</code> to an <code>str</code> bears no practical sense, regretfully being a routine in many subject areas.</p>
|
||||
<p><strong>NB</strong>: sometimes field names are shortened or even omitted (e.g., a heterogenous array is passed instead of a set of named fields) to lessen the amount of traffic. In most cases, this is absolutely meaningless as usually the data is compressed at the protocol level.</p>
|
||||
<h5><a href="#chapter-13-paragraph-5" id="chapter-13-paragraph-5" class="anchor">5. Naming Implies Typing</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-6" id="chapter-13-paragraph-6" class="anchor">6. Naming Implies Typing</a></h5>
|
||||
<p>A field named <code>recipe</code> must be of a <code>Recipe</code> type. A field named <code>recipe_id</code> must contain a recipe identifier that we could find within the <code>Recipe</code> entity.</p>
|
||||
<p>Same for basic types. Arrays must be named in a plural form or as collective nouns, e.g., <code>objects</code>, <code>children</code>. If that's impossible, better add a prefix or a postfix to avoid doubt.</p>
|
||||
<p><strong>Bad</strong>: <code>GET /news</code> — unclear whether a specific news item is returned, or a list of them.</p>
|
||||
@ -1677,7 +1677,7 @@ GET /coffee-machines/{id}/functions
|
||||
<pre><code>GET /v1/coffee-machines/{id}⮠
|
||||
/builtin-functions-list
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-13-paragraph-6" id="chapter-13-paragraph-6" class="anchor">6. Matching Entities Must Have Matching Names and Behave Alike</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-7" id="chapter-13-paragraph-7" class="anchor">7. Matching Entities Must Have Matching Names and Behave Alike</a></h5>
|
||||
<p><strong>Bad</strong>: <code>begin_transition</code> / <code>stop_transition</code><br>
|
||||
— <code>begin</code> and <code>stop</code> terms don't match; developers will have to dig into the docs.</p>
|
||||
<p><strong>Better</strong>: either <code>begin_transition</code> / <code>end_transition</code> or <code>start_transition</code> / <code>stop_transition</code>.</p>
|
||||
@ -1697,7 +1697,7 @@ str_replace(needle, replace, haystack)
|
||||
<li>the first function finds the first occurrence while the second one finds them all, and there is no way to deduce that fact out of the function signatures.</li>
|
||||
</ul>
|
||||
<p>We're leaving the exercise of making these signatures better for the reader.</p>
|
||||
<h5><a href="#chapter-13-paragraph-7" id="chapter-13-paragraph-7" class="anchor">7. Avoid Double Negations</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-8" id="chapter-13-paragraph-8" class="anchor">8. Avoid Double Negations</a></h5>
|
||||
<p><strong>Bad</strong>: <code>"dont_call_me": false</code><br>
|
||||
— humans are bad at perceiving double negation; make mistakes.</p>
|
||||
<p><strong>Better</strong>: <code>"prohibit_calling": true</code> or <code>"avoid_calling": true</code><br>
|
||||
@ -1717,7 +1717,7 @@ str_replace(needle, replace, haystack)
|
||||
}
|
||||
</code></pre>
|
||||
<p>— then developers will have to evaluate the <code>!beans_absence && !cup_absence</code> flag which is equivalent to the <code>!(beans_absence || cup_absence)</code> condition. In this transition, people tend to make mistakes. Avoiding double negations helps little, and regretfully only general piece of advice could be given: avoid the situations when developers have to evaluate such flags.</p>
|
||||
<h5><a href="#chapter-13-paragraph-8" id="chapter-13-paragraph-8" class="anchor">8. Avoid Implicit Type Casting</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-9" id="chapter-13-paragraph-9" class="anchor">9. Avoid Implicit Type Casting</a></h5>
|
||||
<p>This advice is opposite to the previous one, ironically. When developing APIs you frequently need to add a new optional field with a non-empty default value. For example:</p>
|
||||
<pre><code>const orderParams = {
|
||||
contactless_delivery: false
|
||||
@ -1781,7 +1781,7 @@ PUT /v1/users/{id}
|
||||
}
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>: the contradiction with the previous rule lies in the necessity of introducing “negative” flags (the “no limit” flag), which we had to rename to <code>abolish_spending_limit</code>. Though it's a decent name for a negative flag, its semantics is still unobvious, and developers will have to read the docs. That's the way.</p>
|
||||
<h5><a href="#chapter-13-paragraph-9" id="chapter-13-paragraph-9" class="anchor">9. Declare Technical Restrictions Explicitly</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-10" id="chapter-13-paragraph-10" class="anchor">10. Declare Technical Restrictions Explicitly</a></h5>
|
||||
<p>Every field in your API comes with restrictions: the maximum allowed text length, the size of attached documents, the allowed ranges for numeric values, etc. Often, describing those limits is neglected by API developers — either because they consider it obvious, or because they simply don't know the boundaries themselves. This is of course an antipattern: not knowing what are the limits automatically implies that partners' code might stop working at any moment because of the reasons they don't control.</p>
|
||||
<p>Therefore, first, declare the boundaries for every field in the API without any exceptions, and, second, generate proper machine-readable errors describing which exact boundary was violated should such a violation occur.</p>
|
||||
<p>The same reasoning applies to quotas as well: partners must have access to the statistics on which part of the quota they have already used, and the errors in the case of exceeding quotas must be informative.</p>
|
||||
@ -1789,11 +1789,11 @@ PUT /v1/users/{id}
|
||||
<p>The restrictions should apply not only to field sizes, but to list sizes or aggregation intervals as well.</p>
|
||||
<p><strong>Bad</strong>: <code>getOrders()</code> — what if a user made a million of orders?</p>
|
||||
<p><strong>Better</strong>: <code>getOrders({ limit, parameters })</code> — there must be a cap to the amount of processed and returned data, and therefor a possibility to refine the query if a partner needs more data than allowed to return in one request.</p>
|
||||
<h5><a href="#chapter-13-paragraph-10" id="chapter-13-paragraph-10" class="anchor">10. Describe the Retry Policy</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-11" id="chapter-13-paragraph-11" class="anchor">11. Describe the Retry Policy</a></h5>
|
||||
<p>One of the most significant performance-related challenges that nearly any API developer encounters, regardless of whether the API is an internal or a public one, is service denial due to a flood of re-requests. Temporary backend API issues, such as increased response times, can lead to complete server failure if clients begin rapidly repeating requests because of receiving an error or getting a timeout, thus generating a significantly larger than usual workload in a short amount of time.</p>
|
||||
<p>The best practice in such a situation is to require clients to retry API endpoints with increasing intervals (for example, the first retry occurs after one second, the second after two seconds, the third after four seconds, and so on, but no longer than one minute). Of course, in the case of a public API, no one is obliged to comply with such a requirement, but its presence certainly won't make things worse for you. At the very least, some partners will read the documentation and follow your recommendations.</p>
|
||||
<p>Moreover, you can develop a reference implementation of the retry policy in your public SDKs and check it's correctly implemented in open-source modules to your API.</p>
|
||||
<h5><a href="#chapter-13-paragraph-11" id="chapter-13-paragraph-11" class="anchor">11. Count the Amount of Traffic</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-12" id="chapter-13-paragraph-12" class="anchor">12. Count the Amount of Traffic</a></h5>
|
||||
<p>Nowadays the amount of traffic is rarely taken into account — the Internet connection is considered unlimited almost universally. However, it's still not entirely unlimited: with some degree of carelessness, it's always possible to design a system generating the amount of traffic that is uncomfortable even for modern networks.</p>
|
||||
<p>There are three obvious reasons for inflating network traffic:</p>
|
||||
<ul>
|
||||
@ -1802,8 +1802,8 @@ PUT /v1/users/{id}
|
||||
<li>no limits on the data fields set, or too large binary data (graphics, audio, video, etc.) is being transmitted.</li>
|
||||
</ul>
|
||||
<p>All these problems must be solved with setting limitations on field sizes and properly decomposing endpoints. If some entity comprises both “lightweight” data (let's say, the name and the description of the recipe) and “heavy” data (let's say, the promo picture of the 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 (a link to the image, in our case) — this will allow at least setting different cache policies for different kinds of data.</p>
|
||||
<p>As a useful exercise, try modeling the typical lifecycle of a partner's app's main functionality (for example, making a single order) to count the number of requests and the amount of traffic that it takes. It might turn out that the reason for the increased amount of requests / network traffic consumption was a mistake made in the design of state change notification endpoints. We will discuss this issue in detail in the <a href="#api-patterns-push-vs-poll">“Bidirectional Data Flow”</a> chapter of “The API Patterns” section of this book.</p>
|
||||
<h5><a href="#chapter-13-paragraph-12" id="chapter-13-paragraph-12" class="anchor">12. No Results Is a Result</a></h5>
|
||||
<p>As a useful exercise, try modeling the typical lifecycle of a partner's app's main functionality (for example, making a single order) to count the number of requests and the amount of traffic that it takes. It might turn out that the reason for the increased amount of requests / network traffic consumption was a mistake made in the design of state change notification endpoints. We will discuss this issue in detail in the “<a href="#api-patterns-push-vs-poll">Bidirectional Data Flow</a>” chapter of “The API Patterns” section of this book.</p>
|
||||
<h5><a href="#chapter-13-paragraph-13" id="chapter-13-paragraph-13" class="anchor">13. No Results Is a Result</a></h5>
|
||||
<p>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.</p>
|
||||
<p><strong>Bad</strong></p>
|
||||
<pre><code>POST /v1/coffee-machines/search
|
||||
@ -1868,7 +1868,7 @@ POST /v1/offers/search
|
||||
}
|
||||
</code></pre>
|
||||
<p>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.</p>
|
||||
<h5><a href="#chapter-13-paragraph-13" id="chapter-13-paragraph-13" class="anchor">13. Validate Inputs</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-14" id="chapter-13-paragraph-14" class="anchor">14. Validate Inputs</a></h5>
|
||||
<p>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 <code>recipes</code> parameter must not be empty, an error shall be generated (otherwise you violate your own spec).</p>
|
||||
<p>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:</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
@ -1947,7 +1947,7 @@ POST /v1/offers/search
|
||||
strict_mode=true⮠
|
||||
disable_errors=suspicious_coordinates
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-13-paragraph-14" id="chapter-13-paragraph-14" class="anchor">14. Default Values Must Make Sense</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-15" id="chapter-13-paragraph-15" class="anchor">15. Default Values Must Make Sense</a></h5>
|
||||
<p>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.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>POST /v1/coffee-machines/search
|
||||
@ -1975,7 +1975,7 @@ POST /v1/offers/search
|
||||
// Error description
|
||||
}
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-13-paragraph-15" id="chapter-13-paragraph-15" class="anchor">15. Errors Must Be Informative</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-16" id="chapter-13-paragraph-16" class="anchor">16. Errors Must Be Informative</a></h5>
|
||||
<p>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.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>POST /v1/coffee-machines/search
|
||||
@ -2023,7 +2023,7 @@ POST /v1/offers/search
|
||||
}
|
||||
</code></pre>
|
||||
<p>It is also a good practice to return all detectable errors at once to spare developers time.</p>
|
||||
<h5><a href="#chapter-13-paragraph-16" id="chapter-13-paragraph-16" class="anchor">16. Return Unresolvable Errors First</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-17" id="chapter-13-paragraph-17" class="anchor">17. Return Unresolvable Errors First</a></h5>
|
||||
<pre><code>POST /v1/orders
|
||||
{
|
||||
"recipe": "lngo",
|
||||
@ -2046,7 +2046,7 @@ POST /v1/orders
|
||||
}
|
||||
</code></pre>
|
||||
<p>— 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.</p>
|
||||
<h5><a href="#chapter-13-paragraph-17" id="chapter-13-paragraph-17" class="anchor">17. Resolve Error Starting With Big Ones</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-18" id="chapter-13-paragraph-18" class="anchor">18. Resolve Error Starting With Big Ones</a></h5>
|
||||
<p>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.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>POST /v1/orders
|
||||
@ -2090,7 +2090,7 @@ POST /v1/orders
|
||||
}
|
||||
</code></pre>
|
||||
<p>— what was the point of showing the price changed dialog, if the user still can't make an order, even if the price is right? When one of the concurrent orders has finished, and the user is able to commit another one, prices, item availability, and other order parameters will likely need another correction.</p>
|
||||
<h5><a href="#chapter-13-paragraph-18" id="chapter-13-paragraph-18" class="anchor">18. Analyze Potential Error Deadlocks</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-19" id="chapter-13-paragraph-19" class="anchor">19. Analyze Potential Error Deadlocks</a></h5>
|
||||
<p>In complex systems, it might happen that resolving one error leads to another one, and vice versa.</p>
|
||||
<pre><code>// Create an order
|
||||
// with a paid delivery
|
||||
@ -2129,7 +2129,7 @@ POST /v1/orders
|
||||
}
|
||||
</code></pre>
|
||||
<p>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.</p>
|
||||
<h5><a href="#chapter-13-paragraph-19" id="chapter-13-paragraph-19" class="anchor">19. Specify Caching Policies and Lifespans of Resources</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-20" id="chapter-13-paragraph-20" class="anchor">20. Specify Caching Policies and Lifespans of Resources</a></h5>
|
||||
<p>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.</p>
|
||||
<p>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?</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
@ -2169,11 +2169,11 @@ GET /price?recipe=lungo⮠
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-13-paragraph-20" id="chapter-13-paragraph-20" class="anchor">20. Keep the Precision of Fractional Numbers Intact</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-21" id="chapter-13-paragraph-21" class="anchor">21. Keep the Precision of Fractional Numbers Intact</a></h5>
|
||||
<p>If the protocol allows, fractional numbers with fixed precision (like money sums) must be represented as a specially designed type like <code>Decimal</code> or its equivalent.</p>
|
||||
<p>If there is no <code>Decimal</code> type in the protocol (for instance, JSON doesn't have one), you should either use integers (e.g., apply a fixed multiplicator) or strings.</p>
|
||||
<p>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 <code>00:20</code> instead of <code>0.33333…</code>), or provide an SDK to work with this data, or as a last resort describe the rounding principles in the documentation.</p>
|
||||
<h5><a href="#chapter-13-paragraph-21" id="chapter-13-paragraph-21" class="anchor">21. All API Operations Must Be Idempotent</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-22" id="chapter-13-paragraph-22" class="anchor">22. All API Operations Must Be Idempotent</a></h5>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
@ -2240,12 +2240,12 @@ X-Idempotency-Token: <token>
|
||||
<li>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;</li>
|
||||
<li>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.</li>
|
||||
</ul>
|
||||
<h5><a href="#chapter-13-paragraph-22" id="chapter-13-paragraph-22" class="anchor">22. Don't Invent Security Practices</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-23" id="chapter-13-paragraph-23" class="anchor">23. Don't Invent Security Practices</a></h5>
|
||||
<p>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.</p>
|
||||
<p><strong>First</strong>, almost all security-enhancing procedures for every kind of operation <em>are already invented</em>. 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 <a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">Man-in-the-Middle attack</a> as a TLS connection with mutual certificate pinning.</p>
|
||||
<p><strong>Second</strong>, 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 <a href="https://en.wikipedia.org/wiki/Timing_attack">timing attack</a>, and your webserver, to the <a href="https://capec.mitre.org/data/definitions/105.html">request splitting attack</a>.</p>
|
||||
<p>Just in case: any APIs must be provided over TLS 1.2 or higher (better 1.3).</p>
|
||||
<h5><a href="#chapter-13-paragraph-23" id="chapter-13-paragraph-23" class="anchor">23. Help Partners With Security</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-24" id="chapter-13-paragraph-24" class="anchor">24. Help Partners With Security</a></h5>
|
||||
<p>It is equally important to provide such interfaces to partners that would minimize possible security problems for them.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>// Allows partners for setting
|
||||
@ -2302,19 +2302,19 @@ X-Dangerously-Allow-Raw-Value: true
|
||||
}
|
||||
</code></pre>
|
||||
<p>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.</p>
|
||||
<h5><a href="#chapter-13-paragraph-24" id="chapter-13-paragraph-24" class="anchor">24. Use Globally Unique Identifiers</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-25" id="chapter-13-paragraph-25" class="anchor">25. Use Globally Unique Identifiers</a></h5>
|
||||
<p>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., <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)">UUID-4</a>). It might turn out to be extremely useful if you need to merge data from several sources under a single identifier.</p>
|
||||
<p>In general, we tend to advise using urn-like identifiers, e.g. <code>urn:order:<uuid></code> (or just <code>order:<uuid></code>). 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.</p>
|
||||
<p>One important implication: <strong>never use increasing numbers as external identifiers</strong>. 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.</p>
|
||||
<h5><a href="#chapter-13-paragraph-25" id="chapter-13-paragraph-25" class="anchor">25. Stipulate Future Restrictions</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-26" id="chapter-13-paragraph-26" class="anchor">26. Stipulate Future Restrictions</a></h5>
|
||||
<p>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.</p>
|
||||
<p>You are not obliged to actually generate those exceptions, but you might stipulate this possibility in the docs. For example, you might describe the <code>429 Too Many Requests</code> error or captcha redirect but implement the functionality when it's actually needed.</p>
|
||||
<p>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.</p>
|
||||
<p><strong>NB</strong>: this rule has an important implication: <strong>always separate endpoints for different API families</strong>. (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).</p>
|
||||
<h5><a href="#chapter-13-paragraph-26" id="chapter-13-paragraph-26" class="anchor">26. No Bulk Access to Sensitive Data</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-27" id="chapter-13-paragraph-27" class="anchor">27. No Bulk Access to Sensitive Data</a></h5>
|
||||
<p>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 <em>no</em> 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.</p>
|
||||
<p>Often, making such offloads on an ad-hoc basis, i.e., bypassing the API, is a reasonable practice.</p>
|
||||
<h5><a href="#chapter-13-paragraph-27" id="chapter-13-paragraph-27" class="anchor">27. Localization and Internationalization</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-28" id="chapter-13-paragraph-28" class="anchor">28. Localization and Internationalization</a></h5>
|
||||
<p>All endpoints must accept language parameters (for example, in a form of the <code>Accept-Language</code> header), even if they are not being used currently.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
@ -2484,7 +2484,7 @@ GET /v1/runtimes/{runtime_id}/state
|
||||
<pre><code>// Terminates the runtime
|
||||
POST /v1/runtimes/{id}/terminate
|
||||
</code></pre><div class="page-break"></div><h2><a href="#section-3" class="anchor" id="section-3">[Work in Progress] Section II. The API Patterns</a></h2><h3><a href="#api-patterns-context" class="anchor" id="api-patterns-context">Chapter 15. On Design Patterns in the API Context</a><a href="#chapter-15" class="secondary-anchor" id="chapter-15"> </a></h3>
|
||||
<p>The concept of <a href="https://en.wikipedia.org/wiki/Software_design_pattern#History">“Patterns”</a> in the field of software engineering was introduced by Kent Beck and Ward Cunningham in 1987 and popularized by “The Gang of Four” (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in their book “Design Patterns: Elements of Reusable Object-Oriented Software,” which was published in 1994. According to the most widespread definition, a software design pattern is a “general, reusable solution to a commonly occurring problem within a given context.”</p>
|
||||
<p>The concept of “<a href="https://en.wikipedia.org/wiki/Software_design_pattern#History">Patterns</a>” in the field of software engineering was introduced by Kent Beck and Ward Cunningham in 1987 and popularized by “The Gang of Four” (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in their book “Design Patterns: Elements of Reusable Object-Oriented Software,” which was published in 1994. According to the most widespread definition, a software design pattern is a “general, reusable solution to a commonly occurring problem within a given context.”</p>
|
||||
<p>If we talk about APIs, especially those to which developers are end users (e.g., frameworks or operating system interfaces), the classical software design patterns are well applicable to them. Indeed, many examples in the previous Section of this book are just about applying some design patterns.</p>
|
||||
<p>However, if we try to extend this approach to include API development in general, we will soon find that many typical API design issues are high-level and can't be reduced to basic software patterns. Let's say, caching resources (and invalidating the cache) or organizing paginated access are not covered in classical writings.</p>
|
||||
<p>In this Section, we will specify those API design problems that we see as the most important ones. We are not aiming to encompass <em>every</em> problem, let alone every solution, and rather focus on describing approaches to solving typical problems with their pros and cons. We do understand that readers familiar with the works of “The Gang of Four,” Grady Booch, and Martin Fowler might expect a more systematic approach and greater depth of outreach from a section called “The API Patterns,” and we apologize to them in advance.</p>
|
||||
@ -2628,7 +2628,7 @@ try {
|
||||
}
|
||||
</code></pre>
|
||||
<p>As orders are created much more rarely than read, we might significantly increase the system performance if we drop the requirement of returning the most recent state of the resource from the state retrieval endpoints. The versioning will help us avoid possible problems: creating an order will still be impossible unless the client has the actual version. In fact, we transited to the <a href="https://en.wikipedia.org/wiki/Consistency_model#Eventual_consistency">eventual consistency</a> model: the client will be able to fulfill its request <em>sometime</em> when it finally gets the actual data. In modern microservice architectures, eventual consistency is rather an industrial standard, and it might be close to impossible to achieve the opposite, i.e., strict consistency.</p>
|
||||
<p><strong>NB</strong>: let us stress that you might choose the approach only in the case of exposing new APIs. If you're already providing an endpoint implementing some consistency model, you can't just lower the consistency level (for instance, introduce eventual consistency instead of the strict one) even if you never documented the behavior. This will be discussed in detail in the <a href="#back-compat-iceberg-waterline">“On the Waterline of the Iceberg”</a> chapter of “The Backward Compatibility” section of this book.</p>
|
||||
<p><strong>NB</strong>: let us stress that you might choose the approach only in the case of exposing new APIs. If you're already providing an endpoint implementing some consistency model, you can't just lower the consistency level (for instance, introduce eventual consistency instead of the strict one) even if you never documented the behavior. This will be discussed in detail in the “<a href="#back-compat-iceberg-waterline">On the Waterline of the Iceberg</a>” chapter of “The Backward Compatibility” section of this book.</p>
|
||||
<p>Choosing weak consistency instead of a strict one, however, brings some disadvantages. For instance, we might require partners to wait until they get the actual resource state to make changes — but it is quite unobvious for partners (and actually inconvenient) they must be prepared to wait for changes they made themselves to propagate.</p>
|
||||
<pre><code>// Creates an order
|
||||
const api = await api
|
||||
@ -2639,7 +2639,7 @@ const pendingOrders = await api.
|
||||
// The list is empty
|
||||
</code></pre>
|
||||
<p>If strict consistency is not guaranteed, the second call might easily return an empty result as it reads data from a replica, and the newest order might not have hit it yet.</p>
|
||||
<p>An important pattern that helps in this situation is implementing the <a href="https://en.wikipedia.org/wiki/Consistency_model#Read-your-writes_consistency">“read-your-writes”</a> model, i.e., guaranteeing that clients observe the changes they have just made. The consistency might be lifted to the read-your-writes level by making clients pass some token that describes the last changes known to the client.</p>
|
||||
<p>An important pattern that helps in this situation is implementing the “<a href="https://en.wikipedia.org/wiki/Consistency_model#Read-your-writes_consistency">read-your-writes</a>” model, i.e., guaranteeing that clients observe the changes they have just made. The consistency might be lifted to the read-your-writes level by making clients pass some token that describes the last changes known to the client.</p>
|
||||
<pre><code>const order = await api
|
||||
.createOrder(…);
|
||||
const pendingOrders = await api.
|
||||
@ -2675,7 +2675,7 @@ const pendingOrders = await api.
|
||||
<p>There is also an important question regarding the default behavior of the server if no version token was passed. Theoretically, in this case, master data should be returned, as the absence of the token might be the result of an app crash and subsequent restart or corrupted data storage. However, this implies an additional load on the master node.</p>
|
||||
<h4>Evaluating the Risks of Switching to Eventual Consistency</h4>
|
||||
<p>Let us state an important assertion: the methods of solving architectural problems we're discussing in this section <em>are probabilistic</em>. Abolishing strict consistency means that, even if all components of the system work perfectly, client errors will still occur — and we may only try to lessen their numbers for typical usage profiles.</p>
|
||||
<p><strong>NB</strong>: the “typical usage profile” stipulation is important: an API implies the variability of client scenarios, and API usage cases might fall into several groups, each featuring quite different error profiles. The classical example is client APIs (where it's an end user who makes actions and waits for results) versus server APIs (where the execution time is per se not so important — but let's say mass parallel execution might be). If this happens, it's a strong signal to make a family of API products covering different usage scenarios, as we will discuss in <a href="#api-product-range">“The API Services Range”</a> chapter of “The API Product” section of this book.</p>
|
||||
<p><strong>NB</strong>: the “typical usage profile” stipulation is important: an API implies the variability of client scenarios, and API usage cases might fall into several groups, each featuring quite different error profiles. The classical example is client APIs (where it's an end user who makes actions and waits for results) versus server APIs (where the execution time is per se not so important — but let's say mass parallel execution might be). If this happens, it's a strong signal to make a family of API products covering different usage scenarios, as we will discuss in “<a href="#api-product-range">The API Services Range</a>” chapter of “The API Product” section of this book.</p>
|
||||
<p>Let's return to the coffee example, and imagine we implemented the following scheme:</p>
|
||||
<ul>
|
||||
<li>optimistic concurrency control (through, let's say, the id of the last user's order)</li>
|
||||
@ -2740,8 +2740,8 @@ const pendingOrders = await api.
|
||||
<p>The asynchronous call pattern is useful for solving other practical tasks as well:</p>
|
||||
<ul>
|
||||
<li>caching operation results and providing links to them (implying that if the client needs to reread the operation result or share it with another client, it might use the task identifier to do so)</li>
|
||||
<li>ensuring operation idempotency (through introducing the task confirmation step we will actually get the draft-commit system as discussed in the <a href="#api-design-describing-interfaces">“Describing Final Interfaces”</a> chapter)</li>
|
||||
<li>naturally improving resilience to peak loads on the service as the new tasks will be queuing up (possibly prioritized) in fact implementing the <a href="https://en.wikipedia.org/wiki/Token_bucket">“token bucket”</a> technique</li>
|
||||
<li>ensuring operation idempotency (through introducing the task confirmation step we will actually get the draft-commit system as discussed in the “<a href="#api-design-describing-interfaces">Describing Final Interfaces</a>” chapter)</li>
|
||||
<li>naturally improving resilience to peak loads on the service as the new tasks will be queuing up (possibly prioritized) in fact implementing the “<a href="https://en.wikipedia.org/wiki/Token_bucket">token bucket</a>” technique</li>
|
||||
<li>organizing interaction in the cases of very long-lasting operations that require more time than typical timeouts (which are tens of seconds in the case of network calls) or can take unpredictable time.</li>
|
||||
</ul>
|
||||
<p>Also, asynchronous communication is more robust from a future API development point of view: request handling procedures might evolve towards prolonging and extending the asynchronous execution pipelines whereas synchronous handlers must retain reasonable execution times which puts certain restrictions on possible internal architecture.</p>
|
||||
@ -2797,7 +2797,7 @@ const pendingOrders = await api.
|
||||
status: "new"
|
||||
}, …]}
|
||||
</code></pre>
|
||||
<p>However, an attentive reader might notice that this interface violates the recommendation we previously gave in the <a href="#api-design-describing-interfaces">“Describing Final Interfaces”</a> chapter: the returned data volume must be limited, but there are no restrictions in our design. This problem was already present in the previous versions of the endpoint, but abolishing asynchronous order creation makes it much worse. The task creation operation must work as quickly as possible, and therefore, almost all limit checks are to be executed asynchronously. As a result, a client might easily create a large number of ongoing tasks which would potentially inflate the size of the <code>getOngoingOrders</code> response.</p>
|
||||
<p>However, an attentive reader might notice that this interface violates the recommendation we previously gave in the “<a href="#api-design-describing-interfaces">Describing Final Interfaces</a>” chapter: the returned data volume must be limited, but there are no restrictions in our design. This problem was already present in the previous versions of the endpoint, but abolishing asynchronous order creation makes it much worse. The task creation operation must work as quickly as possible, and therefore, almost all limit checks are to be executed asynchronously. As a result, a client might easily create a large number of ongoing tasks which would potentially inflate the size of the <code>getOngoingOrders</code> response.</p>
|
||||
<p><strong>NB</strong>: having <em>no limit at all</em> on order task creation is unwise, and there must be some (involving as lightweight checks as possible). Let us, however, focus on the response size issue in this chapter.</p>
|
||||
<p>Fixing this problem is rather simple: we might introduce a limit for the items returned in the response, and allow passing filtering and sorting parameters, like this:</p>
|
||||
<pre><code>api.getOngoingOrders({
|
||||
@ -2969,7 +2969,7 @@ GET /v1/partners/{id}/offers/history⮠
|
||||
older_than=<item_id>&limit=<limit>
|
||||
</code></pre>
|
||||
<p>The first request format allows for implementing the first scenario, i.e., retrieving the fresh portion of the data. Conversely, the second format makes it possible to consistently iterate over the data to fulfill the second scenario. Importantly, the second request is cacheable as the tail of the list never changes.</p>
|
||||
<p><strong>NB</strong>: in the <a href="#api-design-describing-interfaces">“Describing Final Interfaces”</a> chapter we recommended avoiding exposing incremental identifiers in publicly accessible APIs. Note that the scheme described above might be augmented to comply with this rule by exposing some arbitrary secondary identifiers. The requirement is that these identifiers might be unequivocally converted into monotonous ones.</p>
|
||||
<p><strong>NB</strong>: in the “<a href="#api-design-describing-interfaces">Describing Final Interfaces</a>” chapter we recommended avoiding exposing incremental identifiers in publicly accessible APIs. Note that the scheme described above might be augmented to comply with this rule by exposing some arbitrary secondary identifiers. The requirement is that these identifiers might be unequivocally converted into monotonous ones.</p>
|
||||
<p>Another possible anchor to rely on is the record creation date. However, this approach is harder to implement for the following reasons:</p>
|
||||
<ul>
|
||||
<li>Creation dates for two records might be identical, especially if the records are mass-generated programmatically. In the worst-case scenario, it might happen that at some specific moment, more records were created than one request page contains making it impossible to traverse them.</li>
|
||||
@ -3135,7 +3135,7 @@ GET /v1/orders/created-history⮠
|
||||
</ul>
|
||||
<p>To integrate via a <em>webhook</em>, a partner specifies a URL of their own message processing server, and the API provider calls this endpoint to notify about status changes.</p>
|
||||
<p>Let us imagine that in our coffee example the partner has a backend capable of processing newly created orders to be processed by partner's coffee shops, and we need to organize such communication. Realizing this task comprise several steps:</p>
|
||||
<h5><a href="#chapter-21-paragraph-4" id="chapter-21-paragraph-4" class="anchor">4. Negotiate a Contract</a></h5>
|
||||
<h5><a href="#chapter-21-paragraph-4" id="chapter-21-paragraph-4" class="anchor">1. Negotiate a Contract</a></h5>
|
||||
<p>Depending on how important the partner is for our business, different options are possible:</p>
|
||||
<ul>
|
||||
<li>The API vendor might develop the functionality of calling the partner's <em>webhook</em> utilizing a protocol proposed by the partner</li>
|
||||
@ -3143,9 +3143,9 @@ GET /v1/orders/created-history⮠
|
||||
<li>Any combination of the above</li>
|
||||
</ul>
|
||||
<p>What is important is that the <em>must</em> be a formal contract (preferably in a form of a specification) for <em>webhook</em>'s request and response formats and all the errors that might happen.</p>
|
||||
<h5><a href="#chapter-21-paragraph-5" id="chapter-21-paragraph-5" class="anchor">5. Agree on Authorization and Authentication Methods</a></h5>
|
||||
<h5><a href="#chapter-21-paragraph-5" id="chapter-21-paragraph-5" class="anchor">2. Agree on Authorization and Authentication Methods</a></h5>
|
||||
<p>As a <em>webhook</em> is a callback channel, you will need to develop a separate authorization system to deal with it as it's <em>partners</em> duty to check that the request is genuinely coming from the API backend, not vice versa. We reiterate here our strictest recommendation to stick to existing standard techniques, for example, <a href="https://en.wikipedia.org/wiki/Mutual_authentication#mTLS">mTLS</a>; though in the real world, you will likely have to use archaic methods like fixing the caller server's IP address.</p>
|
||||
<h5><a href="#chapter-21-paragraph-6" id="chapter-21-paragraph-6" class="anchor">6. Develop an Interface for Setting the URL of a</a><em>Webhook</em></h5>
|
||||
<h5><a href="#chapter-21-paragraph-6" id="chapter-21-paragraph-6" class="anchor">3. Develop an Interface for Setting the URL of a</a><em>Webhook</em></h5>
|
||||
<p>As the callback endpoint is developed by partners, we do not know its URL beforehand. It implies some interface must exist for setting this URL and authorized public keys (probably in a form of a control panel for partners).</p>
|
||||
<p><strong>Importantly</strong>, the operation of setting a <em>webhook</em> URL is to be treated as a potentially hazardous one. It is highly desirable to request a second authentication factor to authorize the operations as a potential attacker wreak a lot of havoc if there is a vulnerability in the procedure:</p>
|
||||
<ul>
|
||||
@ -3186,7 +3186,7 @@ GET /v1/orders/created-history⮠
|
||||
</ol>
|
||||
<h4>Message Queues</h4>
|
||||
<p>As for internal APIs, the <em>webhook</em> technology (i.e., the possibility to programmatically define a callback URL) is either not needed at all or is replaced with the <a href="https://en.wikipedia.org/wiki/Web_Services_Discovery">Service Discovery</a> protocol as services comprising a single backend are symmetrically able to call each other. However, the problems of callback-based integration discussed above are equally actual for internal calls. Requesting an internal API might result in a false-negative mistake, internal clients might be unaware that ordering is not guaranteed, etc.</p>
|
||||
<p>To solve these problems, and also to ensure better horizontal scalability, <a href="https://en.wikipedia.org/wiki/Message_queue">message queues</a> were developed, most notably numerous pub/sub pattern implementations. At present moment, pub/sub-based architectures are very popular in enterprise software development, up to switch any inter-service communication to message queues.</p>
|
||||
<p>To solve these problems, and also to ensure better horizontal scalability, <a href="https://en.wikipedia.org/wiki/Message_queue">message queues</a> were developed, most notably numerous pub/sub pattern implementations. At present moment, pub/sub-based architectures are very popular in enterprise software development, up to switching any inter-service communication to message queues.</p>
|
||||
<p><strong>NB</strong>: let us note that everything comes with a price, and these delivery guarantees and horizontal scalability are not an exclusion:</p>
|
||||
<ul>
|
||||
<li>all communication becomes eventually consistent with all the implications</li>
|
||||
@ -3299,7 +3299,7 @@ GET /v1/orders/created-history⮠
|
||||
<li>finally, stating all three numbers (major version, minor version, and patch) allows for fixing a concrete API release with all its specificities (and errors), which — theoretically — means that the integration will remain operable till this version is physically available.</li>
|
||||
</ul>
|
||||
<p>Of course, preserving minor versions infinitely isn't possible (partly because of security and compliance issues that tend to pile up). However, providing such access for a reasonable period of time is rather a hygienic norm for popular APIs.</p>
|
||||
<p><strong>NB</strong>. Sometimes to defend the single accessible API version concept, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backward compatibility as it might be relying on some un-versioned services (for example, some data in the DB that are shared between all the API versions). We, however, consider this an additional reason to isolate such dependencies (see <a href="#back-compat-serenity-notepad">“The Serenity Notepad”</a> chapter) as it means that changes to these subsystems might lead to the inoperability of the API.</p><div class="page-break"></div><h3><a href="#back-compat-iceberg-waterline" class="anchor" id="back-compat-iceberg-waterline">Chapter 27. On the Waterline of the Iceberg</a><a href="#chapter-27" class="secondary-anchor" id="chapter-27"> </a></h3>
|
||||
<p><strong>NB</strong>. Sometimes to defend the single accessible API version concept, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backward compatibility as it might be relying on some un-versioned services (for example, some data in the DB that are shared between all the API versions). We, however, consider this an additional reason to isolate such dependencies (see “<a href="#back-compat-serenity-notepad">The Serenity Notepad</a>” chapter) as it means that changes to these subsystems might lead to the inoperability of the API.</p><div class="page-break"></div><h3><a href="#back-compat-iceberg-waterline" class="anchor" id="back-compat-iceberg-waterline">Chapter 27. On the Waterline of the Iceberg</a><a href="#chapter-27" class="secondary-anchor" id="chapter-27"> </a></h3>
|
||||
<p>Before we start talking about the extensible API design, we should discuss the hygienic minimum. A huge number of problems would have never happened if API vendors had paid more attention to marking their area of responsibility.</p>
|
||||
<h4>Provide a Minimal Amount of Functionality</h4>
|
||||
<p>At any moment in its lifetime, your API is like an iceberg: it comprises an observable (i.e., documented) part and a hidden one, undocumented. If the API is designed properly, these two parts correspond to each other just like the above-water and under-water parts of a real iceberg do, i.e. one to ten. Why so? Because of two obvious reasons.</p>
|
||||
@ -3509,7 +3509,7 @@ POST /v1/recipes
|
||||
}
|
||||
</code></pre>
|
||||
<p>At first glance, again, it looks like a reasonably simple interface, explicitly decomposed into abstraction levels. But let us imagine the future — what would happen with this interface when our system evolves further?</p>
|
||||
<p>The first problem is obvious to those who read the <a href="#api-design-describing-interfaces">“Describing Final Interfaces”</a> chapter thoroughly: product properties must be localized. That will lead us to the first change:</p>
|
||||
<p>The first problem is obvious to those who read the “<a href="#api-design-describing-interfaces">Describing Final Interfaces</a>” chapter thoroughly: product properties must be localized. That will lead us to the first change:</p>
|
||||
<pre><code>"product_properties": {
|
||||
// "l10n" is the standard abbreviation
|
||||
// for "localization"
|
||||
@ -3645,7 +3645,7 @@ PUT /formatters/volume/ru/US
|
||||
…
|
||||
}
|
||||
</code></pre>
|
||||
<p>We should also note that providing a newly created entity identifier by the requesting side isn't exactly the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, we're risking getting lots of collisions on recipe names used by different partners, so we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the <a href="#api-design-describing-interfaces">“Describing Final Interfaces”</a> chapter.</p>
|
||||
<p>We should also note that providing a newly created entity identifier by the requesting side isn't exactly the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, we're risking getting lots of collisions on recipe names used by different partners, so we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the “<a href="#api-design-describing-interfaces">Describing Final Interfaces</a>” chapter.</p>
|
||||
<pre><code>POST /v1/recipes/custom
|
||||
{
|
||||
// The first part of the composite
|
||||
@ -3662,7 +3662,7 @@ PUT /formatters/volume/ru/US
|
||||
}
|
||||
</code></pre>
|
||||
<p>Also note that this format allows us to maintain an important extensibility point: different partners might have both shared and isolated namespaces. Furthermore, we might introduce special namespaces (like <code>common</code>, for example) to allow editing standard recipes (and thus organizing our own recipes backoffice).</p>
|
||||
<p><strong>NB</strong>: a mindful reader might have noted that this technique was already used in our API study much earlier in the <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> chapter with regards to the “program” and “program run” entities. Indeed, we might do it without the <code>program-matcher</code> endpoint and make it this way:</p>
|
||||
<p><strong>NB</strong>: a mindful reader might have noted that this technique was already used in our API study much earlier in the “<a href="#api-design-separating-abstractions">Separating Abstraction Levels</a>” chapter with regards to the “program” and “program run” entities. Indeed, we might do it without the <code>program-matcher</code> endpoint and make it this way:</p>
|
||||
<pre><code>GET /v1/recipes/{id}/run-data/{api_type}
|
||||
→
|
||||
{ /* A description, how to
|
||||
@ -3676,7 +3676,7 @@ PUT /formatters/volume/ru/US
|
||||
<li>depending on the API type, run some specific commands.</li>
|
||||
</ul>
|
||||
<p>Obviously, such an interface is absolutely unacceptable, simply because in the majority of use cases developers don't care at all, which API type the specific coffee machine runs. To avoid the necessity of introducing such bad interfaces we created a new “program” entity, which constitutes merely a context identifier, just like a “recipe” entity does. A <code>program_run_id</code> entity is also organized in this manner, it also possesses no specific properties, being <em>just</em> a program run identifier.</p><div class="page-break"></div><h3><a href="#back-compat-weak-coupling" class="anchor" id="back-compat-weak-coupling">Chapter 30. Weak Coupling</a><a href="#chapter-30" class="secondary-anchor" id="chapter-30"> </a></h3>
|
||||
<p>In the previous chapter, we've demonstrated how breaking strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. But let us return to the question we have previously mentioned in the <a href="#back-compat-abstracting-extending">“Extending through Abstracting”</a> chapter: how should we parametrize the order preparation process implemented via a third-party API? In other words, what <em>is</em> the <code>order_execution_endpoint</code> required in the API type registration handler?</p>
|
||||
<p>In the previous chapter, we've demonstrated how breaking strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. But let us return to the question we have previously mentioned in the “<a href="#back-compat-abstracting-extending">Extending through Abstracting</a>” chapter: how should we parametrize the order preparation process implemented via a third-party API? In other words, what <em>is</em> the <code>order_execution_endpoint</code> required in the API type registration handler?</p>
|
||||
<pre><code>PUT /v1/api-types/{api_type}
|
||||
{
|
||||
…
|
||||
@ -3724,7 +3724,7 @@ PUT /formatters/volume/ru/US
|
||||
<li>the higher-level program API level doesn't actually know how the execution of its commands works; it formulates the tasks at its own level of understanding: brew this recipe, send user's requests to a partner, allow the user to collect their order;</li>
|
||||
<li>the underlying program execution API level doesn't care what other same-level implementations exist; it just interprets those parts of the task that make sense to it.</li>
|
||||
</ul>
|
||||
<p>If we take a look at the principles described in the previous chapter, we would find that this principle was already formulated: we need to describe <em>informational contexts</em> at every abstraction level and design a mechanism to translate them between levels. Furthermore, in a more general sense, we formulated it as early as in “The Data Flow” paragraph of the <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> chapter.</p>
|
||||
<p>If we take a look at the principles described in the previous chapter, we would find that this principle was already formulated: we need to describe <em>informational contexts</em> at every abstraction level and design a mechanism to translate them between levels. Furthermore, in a more general sense, we formulated it as early as in “The Data Flow” paragraph of the “<a href="#api-design-separating-abstractions">Separating Abstraction Levels</a>” chapter.</p>
|
||||
<p>In our case we need to implement the following mechanisms:</p>
|
||||
<ul>
|
||||
<li>running a program creates a corresponding context comprising all the essential parameters;</li>
|
||||
@ -3866,7 +3866,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<h4>Delegate!</h4>
|
||||
<p>From what was said, one more important conclusion follows: doing a real job, i.e., implementing some concrete actions (making coffee, in our case) should be delegated to the lower levels of the abstraction hierarchy. If the upper levels try to prescribe some specific implementation algorithms, then (as we have demonstrated on the <code>order_execution_endpoint</code> example) we will soon face a situation of inconsistent methods and interaction protocols nomenclature, most of which have no specific meaning when we talk about some specific hardware context.</p>
|
||||
<p>Contrariwise, applying the paradigm of concretizing the contexts at each new abstraction level, we will eventually fall into the bunny hole deep enough to have nothing to concretize: the context itself unambiguously matches the functionality we can programmatically control. And at that level, we must stop detailing contexts further, and just realize the algorithms needed. It's worth mentioning that the abstraction deepness for different underlying platforms might vary.</p>
|
||||
<p><strong>NB</strong>. In the <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> chapter we have illustrated exactly this: when we speak about the first coffee machine API type, there is no need to extend the tree of abstractions further than running programs, but with the second API type, we need one more intermediary abstraction level, namely the runtimes API.</p><div class="page-break"></div><h3><a href="#back-compat-universal-interfaces" class="anchor" id="back-compat-universal-interfaces">Chapter 31. Interfaces as a Universal Pattern</a><a href="#chapter-31" class="secondary-anchor" id="chapter-31"> </a></h3>
|
||||
<p><strong>NB</strong>. In the “<a href="#api-design-separating-abstractions">Separating Abstraction Levels</a>” chapter we have illustrated exactly this: when we speak about the first coffee machine API type, there is no need to extend the tree of abstractions further than running programs, but with the second API type, we need one more intermediary abstraction level, namely the runtimes API.</p><div class="page-break"></div><h3><a href="#back-compat-universal-interfaces" class="anchor" id="back-compat-universal-interfaces">Chapter 31. Interfaces as a Universal Pattern</a><a href="#chapter-31" class="secondary-anchor" id="chapter-31"> </a></h3>
|
||||
<p>Let us summarize what we have written in the three previous chapters:</p>
|
||||
<ol>
|
||||
<li>Extending API functionality is implemented through abstracting: the entity nomenclature is to be reinterpreted so that existing methods become partial (ideally — the most frequent) simplified cases to more general functionality.</li>
|
||||
@ -3890,7 +3890,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<p>So our interface (let us call it <code>ISearchResult</code>) is actually a composition of two other interfaces: <code>IOrderParameters</code> (an entity that allows for creating an order) and <code>ISearchItemViewParameters</code> (some abstract representation of the search result in the UI). This interface split should automatically lead us to additional questions:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>How will we couple the former and the latter? Obviously, these two sub-interfaces are related: the machine-readable price must match the human-readable one, for example. This will naturally lead us to the “formatter” concept described in the <a href="#back-compat-strong-coupling">“Strong Coupling and Related Problems”</a> chapter.</p>
|
||||
<p>How will we couple the former and the latter? Obviously, these two sub-interfaces are related: the machine-readable price must match the human-readable one, for example. This will naturally lead us to the “formatter” concept described in the “<a href="#back-compat-strong-coupling">Strong Coupling and Related Problems</a>” chapter.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>And what is the “abstract representation of the search result in the UI”? Do we have other kinds of search, should the <code>ISearchItemViewParameters</code> interface be a subtype of some even more general interface, or maybe a composition of several such ones?</p>
|
||||
@ -3913,7 +3913,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<li>The interface abstraction principle must be tested either. In theory, you might have considered each entity as an implementation of some interface; in practice, it might happen that you have forgotten something and alternative implementations aren't actually possible. For testing purposes, it's highly desirable to have an alternative realization, even a provisional one, for every interface.</li>
|
||||
</ol>
|
||||
<h5><a href="#chapter-32-paragraph-3" id="chapter-32-paragraph-3" class="anchor">3. Isolate the Dependencies</a></h5>
|
||||
<p>In the case of a gateway API that provides access to some underlying API or aggregates several APIs behind a single façade, there is a strong temptation to proxy the original interface as is, thus not introducing any changes to it and making life much simpler by sparing an effort needed to implement the weak-coupled interaction between services. For example, while developing program execution interfaces as described in the <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> chapter we might have taken the existing first-kind coffee-machine API as a role model and provided it in our API by just proxying the requests and responses as is. Doing so is highly undesirable because of several reasons:</p>
|
||||
<p>In the case of a gateway API that provides access to some underlying API or aggregates several APIs behind a single façade, there is a strong temptation to proxy the original interface as is, thus not introducing any changes to it and making life much simpler by sparing an effort needed to implement the weak-coupled interaction between services. For example, while developing program execution interfaces as described in the “<a href="#api-design-separating-abstractions">Separating Abstraction Levels</a>” chapter we might have taken the existing first-kind coffee-machine API as a role model and provided it in our API by just proxying the requests and responses as is. Doing so is highly undesirable because of several reasons:</p>
|
||||
<ul>
|
||||
<li>usually, you have no guarantees that the partner will maintain backward compatibility or at least keep new versions more or less conceptually akin to the older ones;</li>
|
||||
<li>any partner's problem will automatically ricochet into your customers.</li>
|
||||
@ -3934,7 +3934,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<p>There is an antipattern that occurs frequently: API developers use some internal closed implementations of some methods which exist in the public API. It happens because of two reasons:</p>
|
||||
<ul>
|
||||
<li>often the public API is just an addition to the existing specialized software, and the functionality, exposed via the API, isn't being ported back to the closed part of the project, or the public API developers simply don't know the corresponding internal functionality exists;</li>
|
||||
<li>in the course of extending the API, some interfaces become abstract, but the existing functionality isn't affected; imagine that while implementing the <code>PUT /formatters</code> interface described in the <a href="#back-compat-strong-coupling">“Strong Coupling and Related Problems”</a> chapter API developers have created a new, more general version of the volume formatter but hasn't changed the implementation of the existing one, so it continues working for pre-existing languages.</li>
|
||||
<li>in the course of extending the API, some interfaces become abstract, but the existing functionality isn't affected; imagine that while implementing the <code>PUT /formatters</code> interface described in the “<a href="#back-compat-strong-coupling">Strong Coupling and Related Problems</a>” chapter API developers have created a new, more general version of the volume formatter but hasn't changed the implementation of the existing one, so it continues working for pre-existing languages.</li>
|
||||
</ul>
|
||||
<p>There are obvious local problems with this approach (like the inconsistency in functions' behavior, or the bugs which were not found while testing the code), but also a bigger one: your API might be simply unusable if a developer tries any non-mainstream approach, because of performance issues, bugs, instability, etc., as the API developers themselves never tried to use this public interface for anything important.</p>
|
||||
<p><strong>NB</strong>. The perfect example of avoiding this anti-pattern is the development of compilers; usually, the next compiler's version is compiled with the previous compiler's version.</p>
|
||||
@ -4006,7 +4006,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<li>internal customers employ quite a specific technological stack, and the API is poorly optimized to work with other programming languages / operating systems / frameworks;</li>
|
||||
<li>for external customers, the learning curve will be pretty flat as they can't take a look at the source code or talk to the API developers directly, unlike internal customers that are much more familiar with the API concepts;</li>
|
||||
<li>documentation often covers only some subset of use cases needed by internal customers;</li>
|
||||
<li>the API services ecosystem which we will describe in <a href="#api-product-range">“The API Services Range”</a> chapter usually doesn't exist.</li>
|
||||
<li>the API services ecosystem which we will describe in “<a href="#api-product-range">The API Services Range</a>” chapter usually doesn't exist.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Any resources spent are directed to covering internal customer needs first. It means the following:
|
||||
@ -4063,7 +4063,7 @@ ProgramContext.dispatch = (action) => {
|
||||
</ul>
|
||||
<p>As both approaches are still heuristic, the API product vision is inevitably fuzzy, and it's rather normal: if you could have got a full and clear understanding of what end-user products might be developed on top of your API, you might have developed them on your own behalf, skipping intermediary agents. It is also important to keep in mind that many APIs pass the “terraforming” stage (see the previous chapter) thus preparing the ground for new markets and new types of services — so your idealistic vision of a nearby future where delivering freshly brewed coffee by drones will be a norm of life is to be refined and clarified while new companies providing new kinds of services are coming to the market. (Which in its turn will make an impact on the monetization model: detailing the countenance of the forthcoming will make your abstract KPIs and theoretical benefits of having an API more and more concrete.)</p>
|
||||
<p>The same fuzziness should be kept in mind while making interviews and getting feedback. Software engineers will mainly report the problems they've got with the technical integrations, and rarely speak of business-related issues; meanwhile, business owners care little about the inconvenience of writing code. Both will have some knowledge regarding the end users' problems, but it's usually limited to the market segment the partner operates on.</p>
|
||||
<p>If you do have an access to end users' actions monitoring (see <a href="#api-product-kpi">“The API Key Performance Indicators”</a> chapter), then you might try to analyze the typical user behavior through these logs and understand how users interact with the partners' applications. But you will need to make this analysis on a per-application basis and try to clusterize the most common scenarios.</p>
|
||||
<p>If you do have an access to end users' actions monitoring (see “<a href="#api-product-kpi">The API Key Performance Indicators</a>” chapter), then you might try to analyze the typical user behavior through these logs and understand how users interact with the partners' applications. But you will need to make this analysis on a per-application basis and try to clusterize the most common scenarios.</p>
|
||||
<h4>Checking Product Hypotheses</h4>
|
||||
<p>Apart from the general complexity of formulating the product vision, there are also tactical issues with checking product hypotheses. “The Holy Grail” of product management — that is, creating a cheap (in terms of resource spent) minimal viable product (MVP) — is normally unavailable for an API product manager. The thing is that you can't easily <em>test</em> the solution even if you managed to develop an API MVP: to do so, partners are to <em>develop some code</em>, i.e., invest their money; and if the outcome of the experiment is negative (meaning that the further development looks unpromising), this money will be wasted. Of course, partners will be a little bit skeptical towards such proposals. Thus a “cheap” MVP should include either the compensation for partners' expenses or the budget to develop a reference implementation (i.e., a complementary application that is created to support the API MVP).</p>
|
||||
<p>You might partially solve the problem by making some third-party company release the MVP (for example, in a form of an open-source module published in some developer's personal repository) but then you will struggle with hypothesis validation issues as such modules might easily go unnoticed.</p>
|
||||
@ -4140,7 +4140,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<li>A huge share of customers' inquiries to your customer support service will be generated by the first category of developers: it's much harder for amateurs or beginners to find answers to their questions by themselves, and they will address them to you.</li>
|
||||
<li>At the same time, the second category is much more sensitive to the quality of both the product and customer support, and fulfilling their requests might be non-trivial.</li>
|
||||
</ul>
|
||||
<p>Finally, it's almost impossible in a course of a single product to create an API that will fit well both amateur and professional developers: the former need the maximum simplicity of implementing basic use cases, while the latter seek the ability to adapt the API to match technological stack and development paradigms, and the problems they solve usually require deep customization. We will discuss the matter in <a href="#api-product-range">“The API Services Range”</a> chapter.</p><div class="page-break"></div><h3><a href="#api-product-business-comms" class="anchor" id="api-product-business-comms">Chapter 55. Communicating with Business Owners</a><a href="#chapter-55" class="secondary-anchor" id="chapter-55"> </a></h3>
|
||||
<p>Finally, it's almost impossible in a course of a single product to create an API that will fit well both amateur and professional developers: the former need the maximum simplicity of implementing basic use cases, while the latter seek the ability to adapt the API to match technological stack and development paradigms, and the problems they solve usually require deep customization. We will discuss the matter in “<a href="#api-product-range">The API Services Range</a>” chapter.</p><div class="page-break"></div><h3><a href="#api-product-business-comms" class="anchor" id="api-product-business-comms">Chapter 55. Communicating with Business Owners</a><a href="#chapter-55" class="secondary-anchor" id="chapter-55"> </a></h3>
|
||||
<p>The basics of interacting with business partners are to some extent paradoxically contrary to the basics of communicating with developers:</p>
|
||||
<ul>
|
||||
<li>on one hand, partners are much more loyal and sometimes even enthusiastic regarding opportunities you offer (especially free ones);</li>
|
||||
@ -4163,7 +4163,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<h4>Vertical Scaling of API Services</h4>
|
||||
<p>However, frequently it makes sense to provide several API services manipulating the same functionality. Let us remind you that there are two kinds of developers: professional ones that seek extensive customization capabilities (as they usually work in big IT companies that have a specific mindset towards integrations), and semi-professionals who just need the gentlest possible learning curve. The only way to cover the needs of both categories is to develop a range of products with different entry thresholds and requirements for developers' professional level. We might name several API sub-types, ordered from the most technically demanding to less complex ones.</p>
|
||||
<ol>
|
||||
<li>The most advanced level is that of physical APIs and the abstractions on top of them. [In our coffee example, the collection of entities describing working with APIs of physical coffee machines, see the <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> and the <a href="#back-compat-weak-coupling">“Weak Coupling”</a> chapters.]</li>
|
||||
<li>The most advanced level is that of physical APIs and the abstractions on top of them. [In our coffee example, the collection of entities describing working with APIs of physical coffee machines, see the “<a href="#api-design-separating-abstractions">Separating Abstraction Levels</a>” and the “<a href="#back-compat-weak-coupling">Weak Coupling</a>” chapters.]</li>
|
||||
<li>The basic level of working with product entities via formal interfaces. [In our study example, that will be HTTP API for making orders.]</li>
|
||||
<li>Working with product entities might be simplified if SDKs are provided for some popular platforms that tailor API concepts according to the paradigms of those platforms (for those developers who are proficient with specific platforms only that will save a lot of effort on dealing with formal protocols and interfaces).</li>
|
||||
<li>The next simplification step is providing services for code generation. In this service, developers choose one of the pre-built integration templates, customize some options, and got a ready-to-use piece of code that might be simply copy-pasted into the application code (and might be additionally customized by adding some level 1-3 code). This approach is sometimes called “point-and-click programming.” [In the case of our coffee API, an example of such a service might have a form or screen editor for a developer to place UI elements and get the working application code.]</li>
|
||||
@ -4178,7 +4178,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<li>Code generation makes it possible to manipulate the desired form of integrations. For example, if our KPI is a number of searches performed through the API, we might alter the generated code so it will show the search panel in the most convenient position in the app; as partners using code-generation services rarely make any changes in the resulting code, and this will help us in reaching the goal.</li>
|
||||
<li>Finally, ready-to-use components and widgets are under your full control, and you might experiment with functionality exposed through them in partners' applications just as if it was your own service. (However, it doesn't automatically mean that you might draw some profits from having this control; for example, if you're allowing inserting pictures by their direct URL, your control over this integration is rather negligible, so it's generally better to provide those kinds of integration that allow having more control over the functionality in partners' apps.)</li>
|
||||
</ol>
|
||||
<p><strong>NB</strong>. While developing a “vertical” range of APIs, following the principles stated in the <a href="#back-compat-iceberg-waterline">“On the Waterline of the Iceberg”</a> chapter is crucial. You might manipulate widget content and behavior if, and only if, developers can't “escape the sandbox,” i.e., have direct access to low-level objects encapsulated within the widget.</p>
|
||||
<p><strong>NB</strong>. While developing a “vertical” range of APIs, following the principles stated in the “<a href="#back-compat-iceberg-waterline">On the Waterline of the Iceberg</a>” chapter is crucial. You might manipulate widget content and behavior if, and only if, developers can't “escape the sandbox,” i.e., have direct access to low-level objects encapsulated within the widget.</p>
|
||||
<p>In general, you should aim to have each partner using the API services in a manner that maximizes your profit as an API vendor. Where the partner doesn't try to make some unique experience and needs just a typical solution, you would benefit from making them use widgets, which are under your full control and thus ease the API version fragmentation problem and allow for experimenting in order to reach your KPIs. Where the partner possesses some unique expertise in the subject area and develops a unique service on top of your API, you would benefit from allowing full freedom in customizing the integration, so they might cover specific market niches and enjoy the advantage of offering more flexibility compared to services using competing APIs.</p><div class="page-break"></div><h3><a href="#api-product-kpi" class="anchor" id="api-product-kpi">Chapter 57. The API Key Performance Indicators</a><a href="#chapter-57" class="secondary-anchor" id="chapter-57"> </a></h3>
|
||||
<p>As we described in the previous chapters, there are many API monetization models, both direct and indirect. Importantly, most of them are fully or conditionally free for partners, and the direct-to-indirect benefits ratio tends to change during the API lifecycle. That naturally leads us to the question of how exactly shall we measure the API success and what goals are to be set for the product team.</p>
|
||||
<p>Of course, the most explicit metric is money: if your API is monetized directly or attracts visitors to a monetized service, the rest of the chapter will be of little interest to you, maybe just as a case study. If, however, the contribution of the API to the company's income cannot be simply measured, you have to stick to other, synthetic, indicators.</p>
|
||||
@ -4304,7 +4304,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<p><strong>Importantly</strong>, when we talk about “users,” we will have to make duplicate systems to observe them both using tokens (cookies, logins, phone numbers) and IP addresses, as malefactors aren't obliged to preserve the tokens between requests, or might keep a pool of them to impede their exposure.</p>
|
||||
<h5><a href="#chapter-59-paragraph-2" id="chapter-59-paragraph-2" class="anchor">2. Requesting an Additional Authentication Factor</a></h5>
|
||||
<p>As both static and behavioral analyses are heuristic, it's highly desirable to not make decisions based solely on their outcome but rather ask the suspicious users to additionally prove they're making legitimate requests. If such a mechanism is in place, the quality of an anti-fraud system will be dramatically improved, as it allows for increasing system sensitivity and enabling pro-active defense, i.e., asking users to pass the tests in advance.</p>
|
||||
<p>In the case of services for end users, the main method of acquiring the second factor is redirecting to a captcha page. In the case of APIs it might be problematic, especially if you initially neglected the “Stipulate Restrictions” rule we've given in the <a href="#api-design-describing-interfaces">“Describing Final Interfaces”</a> chapter. In many cases, you will have to impose this responsibility on partners (i.e., it will be partners who show captchas and identify users based on the signals received from the API endpoints). This will, of course, significantly impair the convenience of working with the API.</p>
|
||||
<p>In the case of services for end users, the main method of acquiring the second factor is redirecting to a captcha page. In the case of APIs it might be problematic, especially if you initially neglected the “Stipulate Restrictions” rule we've given in the “<a href="#api-design-describing-interfaces">Describing Final Interfaces</a>” chapter. In many cases, you will have to impose this responsibility on partners (i.e., it will be partners who show captchas and identify users based on the signals received from the API endpoints). This will, of course, significantly impair the convenience of working with the API.</p>
|
||||
<p><strong>NB</strong>. Instead of captcha, there might be other actions introducing additional authentication factors. It might be the phone number confirmation or the second step of the 3D-Secure protocol. The important part is that requesting an additional authentication step must be stipulated in the program interface, as it can't be added later in a backwards-compatible manner.</p>
|
||||
<p>Other popular mechanics of identifying robots include offering a bait (“honeypot”) or employing the execution environment checks (starting from rather trivial ones like executing JavaScript on the webpage and ending with sophisticated techniques of checking application integrity checksums).</p>
|
||||
<h5><a href="#chapter-59-paragraph-3" id="chapter-59-paragraph-3" class="anchor">3. Restricting Access</a></h5>
|
||||
@ -4389,7 +4389,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<p>The inverse scenario: partners must pay for technical support, and it's the API developers who answer the questions. It doesn't actually make a significant difference in terms of the quality of the issues (it's still mostly inexperienced developers who can't solve the problem on their own; you will just cut off those who can't afford paid support) but at least you won't have a hiring problem as you might allow yourself the luxury of having engineers for the first line of support.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Partly (or, sometimes, fully) the developer community might help with solving the amateur problems (see the <a href="#api-product-devrel">“Communicating with Developers”</a> chapter). Usually, community members are pretty capable of answering those questions, especially if moderators help them.</p>
|
||||
<p>Partly (or, sometimes, fully) the developer community might help with solving the amateur problems (see the “<a href="#api-product-devrel">Communicating with Developers</a>” chapter). Usually, community members are pretty capable of answering those questions, especially if moderators help them.</p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>Importantly, whatever options you choose, it's still the API developers in the second line of support simply because only they can fully understand the problem and the partners' code. That implies two important consequences:</p>
|
||||
@ -4509,7 +4509,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<p>Finally, the last aspect we would like to shed the light on is managing partners' expectations regarding the further development of the API. If we talk about consumer qualities, APIs differ little from other B2B software products: in both cases, you need to form some understanding of SLA conditions, available features, interface responsiveness and other characteristics that are important for clients. Still, APIs have their specificities</p>
|
||||
<h4>Versioning and Application Lifecycle</h4>
|
||||
<p>Ideally, the API once published should live eternally; but as we all are reasonable people, we do understand it's impossible in the real life. Even if we continue supporting older versions, they will still become outdated eventually, and partners will need to rewrite the code to use newer functionality.</p>
|
||||
<p>The author of this book formulates the rule of issuing new major API versions like this: the period of time after which partners will need to rewrite the code should coincide with the application lifespan in the subject area (see <a href="#back-compat-statement">“The Backward Compatibility Problem Statement”</a> chapter). Apart from updating <em>major</em> versions, sooner or later you will face issues with accessing some outdated <em>minor</em> versions as well. As we mentioned in the <a href="#back-compat-iceberg-waterline">“On the Waterline of the Iceberg”</a> chapter, even fixing bugs might eventually lead to breaking some integrations, and that naturally leads us to the necessity of keeping older <em>minor</em> versions of the API until the partner resolves the problem.</p>
|
||||
<p>The author of this book formulates the rule of issuing new major API versions like this: the period of time after which partners will need to rewrite the code should coincide with the application lifespan in the subject area (see “<a href="#back-compat-statement">The Backward Compatibility Problem Statement</a>” chapter). Apart from updating <em>major</em> versions, sooner or later you will face issues with accessing some outdated <em>minor</em> versions as well. As we mentioned in the “<a href="#back-compat-iceberg-waterline">On the Waterline of the Iceberg</a>” chapter, even fixing bugs might eventually lead to breaking some integrations, and that naturally leads us to the necessity of keeping older <em>minor</em> versions of the API until the partner resolves the problem.</p>
|
||||
<p>In this aspect, integrating with large companies that have a dedicated software engineering department differs dramatically from providing a solution to individual amateur programmers: on one hand, the former are much more likely to find undocumented features and unfixed bugs in your code; on the other hand, because of the internal bureaucracy, fixing the related issues might easily take months, save not years. The common recommendation there is to maintain old minor API versions for a period of time long enough for the most dilatory partner to switch no the newest version.</p>
|
||||
<h4>Supporting Platforms</h4>
|
||||
<p>Another aspect crucial to interacting with large integrators is supporting a zoo of platforms (browsers, programming languages, protocols, operating systems) and their versions. As usual, big companies have their own policies on which platforms they support, and these policies might sometimes contradict common sense. (Let's say, it's rather a time to abandon TLS 1.2, but many integrators continue working through this protocol, or even the earlier ones.)</p>
|
||||
|
BIN
docs/API.en.pdf
BIN
docs/API.en.pdf
Binary file not shown.
BIN
docs/API.ru.epub
BIN
docs/API.ru.epub
Binary file not shown.
@ -1561,11 +1561,11 @@ app.display(offers);
|
||||
<p>Важнейшая задача разработчика API — добиться того, чтобы код, написанный поверх API другими разработчиками, легко читался и поддерживался. Помните, что закон больших чисел работает против вас: если какую-то концепцию или сигнатуру вызова можно понять неправильно, значит, её неизбежно будет понимать неправильно всё большее число партнеров по мере роста популярности API.</p>
|
||||
<p><strong>NB</strong>: примеры, приведённые в этой главе, прежде всего иллюстрируют проблемы консистентности и читабельности, возникающие при разработке API. Мы не ставим здесь цели дать рекомендации по разработке REST API (такого рода советы будут даны в соответствующем разделе) или стандартных библиотек языков программирования — важен не конкретный синтаксис, а общая идея.</p>
|
||||
<p>Важное уточнение под номером ноль:</p>
|
||||
<h5><a href="#chapter-13-paragraph-0" id="chapter-13-paragraph-0" class="anchor">0. Правила не должны применяться бездумно</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-1" id="chapter-13-paragraph-1" class="anchor">0. Правила не должны применяться бездумно</a></h5>
|
||||
<p>Правило — это просто кратко сформулированное обобщение опыта. Они не действуют безусловно и не означают, что можно не думать головой. У каждого правила есть какая-то рациональная причина его существования. Если в вашей ситуации нет причин следовать правилу — значит, следовать ему не нужно.</p>
|
||||
<p>Это соображение применимо ко всем принципам ниже. Если из-за следования правилам у вас получается неудобный, громоздкий, неочевидный API — это повод пересмотреть правила (или API).</p>
|
||||
<p>Важно понимать, что вы вольны вводить свои собственные конвенции. Например, в некоторых фреймворках сознательно отказываются от парных методов <code>set_entity</code> / <code>get_entity</code> в пользу одного метода <code>entity</code> с опциональным параметром. Важно только проявить последовательность в её применении — если такая конвенция вводится, то абсолютно все методы API должны иметь подобную полиморфную сигнатуру, или по крайней мере должен существовать принцип именования, отличающий такие комбинированные методы от обычных вызовов.</p>
|
||||
<h5><a href="#chapter-13-paragraph-1" id="chapter-13-paragraph-1" class="anchor">1. Явное лучше неявного</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-2" id="chapter-13-paragraph-2" class="anchor">2. Явное лучше неявного</a></h5>
|
||||
<p>Из названия любой сущности должно быть очевидно, что она делает, и к каким побочным эффектам может привести её использование.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Отменяет заказ
|
||||
@ -1594,7 +1594,7 @@ orders.calculateAggregatedStats({
|
||||
<p>Два важных следствия:</p>
|
||||
<p><strong>1.1.</strong> Если операция модифицирующая, это должно быть очевидно из сигнатуры. В частности, модифицирующая операция не может называться <code>getSomething</code> или использоваться с HTTP-глаголом <code>GET</code>.</p>
|
||||
<p><strong>1.2.</strong> Если в номенклатуре вашего API есть как синхронные операции, так и асинхронные, то (а)синхронность должна быть очевидна из сигнатур, <strong>либо</strong> должна существовать конвенция именования, позволяющая отличать синхронные операции от асинхронных.</p>
|
||||
<h5><a href="#chapter-13-paragraph-2" id="chapter-13-paragraph-2" class="anchor">2. Указывайте использованные стандарты</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-3" id="chapter-13-paragraph-3" class="anchor">3. Указывайте использованные стандарты</a></h5>
|
||||
<p>К сожалению, человечество не в состоянии договориться о таких простейших вещах, как «с какого дня начинается неделя». Поэтому <em>всегда</em> указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе.</p>
|
||||
<p><strong>Плохо</strong>: <code>"date": "11/12/2020"</code> — существует огромное количество стандартов записи дат, плюс из этой записи невозможно даже понять, что здесь число, а что месяц.</p>
|
||||
<p><strong>Хорошо</strong>: <code>"iso_date": "2020-11-12"</code>.</p>
|
||||
@ -1613,11 +1613,11 @@ orders.calculateAggregatedStats({
|
||||
</code></pre>
|
||||
<p>Отдельное следствие из этого правила — денежные величины <em>всегда</em> должны сопровождаться указанием кода валюты.</p>
|
||||
<p>Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что, как ни сделай, — кто-то останется недовольным. Классический пример такого рода — порядок географических координат («широта-долгота» против «долгота-широта»). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — Блокнот душевного покоя, который будет описан <a href="#back-compat-serenity-notepad">в одноимённой главе</a>.</p>
|
||||
<h5><a href="#chapter-13-paragraph-3" id="chapter-13-paragraph-3" class="anchor">3. Сущности должны именоваться конкретно</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-4" id="chapter-13-paragraph-4" class="anchor">4. Сущности должны именоваться конкретно</a></h5>
|
||||
<p>Избегайте одиночных слов-«амёб» без определённой семантики, таких как get, apply, make.</p>
|
||||
<p><strong>Плохо</strong>: <code>user.get()</code> — неочевидно, что конкретно будет возвращено.</p>
|
||||
<p><strong>Хорошо</strong>: <code>user.get_id()</code>.</p>
|
||||
<h5><a href="#chapter-13-paragraph-4" id="chapter-13-paragraph-4" class="anchor">4. Не экономьте буквы</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-5" id="chapter-13-paragraph-5" class="anchor">5. Не экономьте буквы</a></h5>
|
||||
<p>В XXI веке давно уже нет нужды называть переменные покороче.</p>
|
||||
<p><strong>Плохо</strong>: <code>order.getTime()</code> — неясно, о каком времени идёт речь: время создания заказа, время готовности заказа, время ожидания заказа?…</p>
|
||||
<p><strong>Хорошо</strong>: <code>order.getEstimatedDeliveryTime()</code>.</p>
|
||||
@ -1636,7 +1636,7 @@ strpbrk (str1, str2)
|
||||
</code></pre>
|
||||
<p>— однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение <code>string</code> до <code>str</code> выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.</p>
|
||||
<p><strong>NB</strong>: иногда названия полей сокращают или вовсе опускают (например, возвращают массив разнородных объектов вместо набора именованных полей) в погоне за уменьшением количества трафика. В абсолютном большинстве случаев это бессмысленно, поскольку текстовые данные при передаче обычно дополнительно сжимают на уровне протокола.</p>
|
||||
<h5><a href="#chapter-13-paragraph-5" id="chapter-13-paragraph-5" class="anchor">5. Тип поля должен быть ясен из его названия</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-6" id="chapter-13-paragraph-6" class="anchor">6. Тип поля должен быть ясен из его названия</a></h5>
|
||||
<p>Если поле называется <code>recipe</code> — мы ожидаем, что его значением является сущность типа <code>Recipe</code>. Если поле называется <code>recipe_id</code> — мы ожидаем, что его значением является идентификатор, который мы сможем найти в составе сущности <code>Recipe</code>.</p>
|
||||
<p>То же касается и базовых типов. Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — <code>objects</code>, <code>children</code>; если это невозможно (термин неисчисляем), следует добавить префикс или постфикс, не оставляющий сомнений.</p>
|
||||
<p><strong>Плохо</strong>: <code>GET /news</code> — неясно, будет ли получена какая-то конкретная новость или массив новостей.</p>
|
||||
@ -1656,7 +1656,7 @@ GET /coffee-machines/{id}/functions
|
||||
<pre><code>GET /v1/coffee-machines/{id}⮠
|
||||
/builtin-functions-list
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-13-paragraph-6" id="chapter-13-paragraph-6" class="anchor">6. Подобные сущности должны называться подобно и вести себя подобным образом</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-7" id="chapter-13-paragraph-7" class="anchor">7. Подобные сущности должны называться подобно и вести себя подобным образом</a></h5>
|
||||
<p><strong>Плохо</strong>: <code>begin_transition</code> / <code>stop_transition</code><br>
|
||||
— <code>begin</code> и <code>stop</code> — непарные термины; разработчик будет вынужден рыться в документации.</p>
|
||||
<p><strong>Хорошо</strong>: <code>begin_transition</code> / <code>end_transition</code> либо <code>start_transition</code> / <code>stop_transition</code>.</p>
|
||||
@ -1677,7 +1677,7 @@ str_replace(needle, replace, haystack)
|
||||
<li>первый из методов находит только первое вхождение строки <code>needle</code>, а другой — все вхождения, и об этом поведении никак нельзя узнать из сигнатуры функций.</li>
|
||||
</ul>
|
||||
<p>Упражнение «как сделать эти интерфейсы хорошо» предоставим читателю.</p>
|
||||
<h5><a href="#chapter-13-paragraph-7" id="chapter-13-paragraph-7" class="anchor">7. Избегайте двойных отрицаний</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-8" id="chapter-13-paragraph-8" class="anchor">8. Избегайте двойных отрицаний</a></h5>
|
||||
<p><strong>Плохо</strong>: <code>"dont_call_me": false</code><br>
|
||||
— люди в целом плохо считывают двойные отрицания. Это провоцирует ошибки.</p>
|
||||
<p><strong>Лучше</strong>: <code>"prohibit_calling": true</code> или <code>"avoid_calling": true</code><br>
|
||||
@ -1760,19 +1760,19 @@ PUT /v1/users/{id}
|
||||
}
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>: противоречие с предыдущим советом состоит в том, что мы специально ввели отрицающий флаг («нет лимита»), который по правилу двойных отрицаний пришлось переименовать в <code>abolish_spending_limit</code>. Хотя это и хорошее название для отрицательного флага, семантика его довольно неочевидна, разработчикам придётся как минимум покопаться в документации. Таков путь.</p>
|
||||
<h5><a href="#chapter-13-paragraph-8" id="chapter-13-paragraph-8" class="anchor">8. Декларируйте технические ограничения явно</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-9" id="chapter-13-paragraph-9" class="anchor">9. Декларируйте технические ограничения явно</a></h5>
|
||||
<p>У любого поля в вашем API есть ограничения на допустимые значения: максимальная длина текста, объём прикладываемых документов в мегабайтах, разрешённые диапазоны цифровых значений. Часто разработчики API пренебрегают указанием этих лимитов — либо потому, что считают их очевидными, либо потому, что попросту не знают их сами. Это, разумеется, один большой антипаттерн: незнание пределов использования системы автоматически означает, что код партнёров может в любой момент перестать работать по не зависящим от них причинам.</p>
|
||||
<p>Поэтому, во-первых, указывайте границы допустимых значений для всех без исключения полей в API, и, во-вторых, если эти границы нарушены, генерируйте машиночитаемую ошибку с описанием, какое ограничение на какое поле было нарушено.</p>
|
||||
<p>То же соображение применимо и к квотам: партнёры должны иметь доступ к информации о том, какую долю доступных ресурсов они выбрали, и ошибки в случае превышения квоты должны быть информативными.</p>
|
||||
<h5><a href="#chapter-13-paragraph-9" id="chapter-13-paragraph-9" class="anchor">9. Любые запросы должны быть лимитированы</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-10" id="chapter-13-paragraph-10" class="anchor">10. Любые запросы должны быть лимитированы</a></h5>
|
||||
<p>Ограничения должны быть не только на размеры полей, но и на размеры списков или агрегируемых интервалов.</p>
|
||||
<p><strong>Плохо</strong>: <code>getOrders()</code> — что, если пользователь совершил миллион заказов?</p>
|
||||
<p><strong>Хорошо</strong>: <code>getOrders({ limit, parameters })</code> — должно существовать ограничение сверху на размер обрабатываемых и возвращаемых данных и, соответственно, возможность уточнить запрос, если партнёру всё-таки требуется большее количество данных, чем разрешено обрабатывать в одном запросе.</p>
|
||||
<h5><a href="#chapter-13-paragraph-10" id="chapter-13-paragraph-10" class="anchor">10. Описывайте политику перезапросов</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-11" id="chapter-13-paragraph-11" class="anchor">11. Описывайте политику перезапросов</a></h5>
|
||||
<p>Одна из самых больших проблем с точки зрения производительности, с которой сталкивается почти любой разработчик API, и внутренних, и публичных — это отказ в обслуживании вследствие лавины перезапросов: временные проблемы на бэкенде API (например, повышение времени ответа) могут привести к полной неработоспособности сервера, если клиенты начнут очень быстро повторять запрос, не получив или не дождавшись ответа, сгенерировав, таким образом, кратно большую нагрузку в короткий срок.</p>
|
||||
<p>Лучшая практика в такой ситуации — это требовать, чтобы клиенты перезапрашивали эндпойнты API с увеличивающимся интервалом (скажем, перевый перезапрос происходит через одну секунду, второй — через две, третий через четыре, и так далее, но не больше одной минуты). Конечно, в случае публичного API такое требование никто не обязан соблюдать, но и хуже от его наличия вам точно не станет: хотя бы часть партнёров прочитает документацию и последует вашим рекомендациям.</p>
|
||||
<p>Кроме того, вы можете разработать референсную реализацию политики перезапросов в ваших публичных SDK и следить за правильностью имплементации open-source модулей к вашему API.</p>
|
||||
<h5><a href="#chapter-13-paragraph-11" id="chapter-13-paragraph-11" class="anchor">11. Считайте трафик</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-12" id="chapter-13-paragraph-12" class="anchor">12. Считайте трафик</a></h5>
|
||||
<p>В современном мире такой ресурс, как объём переданного трафика, считать уже почти не принято — считается, что Интернет всюду практически безлимитен. Однако он всё-таки не абсолютно безлимитен: всегда можно спроектировать систему так, что объём трафика окажется некомфортным даже и для современных сетей.</p>
|
||||
<p>Три основные причины раздувания объёма трафика достаточно очевидны:</p>
|
||||
<ul>
|
||||
@ -1782,7 +1782,7 @@ PUT /v1/users/{id}
|
||||
</ul>
|
||||
<p>Все эти проблемы должны решаться через введения ограничений на размеры полей и правильную декомпозицию эндпойнтов. Если в рамках одной сущности необходимо предоставлять как «лёгкие» (скажем, название и описание рецепта), так и «тяжёлые» данные (скажем, промо-фотография напитка, которая легко может по размеру превышать текстовые поля в сотни раз), лучше разделить эндпойнты и отдавать только ссылку для доступа к «тяжёлым» данным (в нашем случае, ссылку на изображение) — это, как минимум, позволит задавать различные политики кэширования для разных данных.</p>
|
||||
<p>Неплохим упражнением здесь будет промоделировать типовой жизненный цикл основной функциональности приложения партнёра (например, выполнение одного заказа) и подсчитать общее количество запросов и объём трафика на один цикл. Причиной слишком большого числа запросов / объёма трафика может оказаться ошибка проектирования подсистемы уведомлений об изменениях состояния. Подробнее этот вопрос мы рассмотрим в главе <a href="#api-patterns-push-vs-poll">«Двунаправленные потоки данных»</a> раздела «Паттерны API».</p>
|
||||
<h5><a href="#chapter-13-paragraph-12" id="chapter-13-paragraph-12" class="anchor">12. Отсутствие результата — тоже результат</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-13" id="chapter-13-paragraph-13" class="anchor">13. Отсутствие результата — тоже результат</a></h5>
|
||||
<p>Если сервер корректно обработал вопрос и никакой внештатной ситуации не возникло — следовательно, это не ошибка. К сожалению, весьма распространён антипаттерн, когда отсутствие результата считается ошибкой.</p>
|
||||
<p><strong>Плохо</strong></p>
|
||||
<pre><code>POST /v1/coffee-machines/search
|
||||
@ -1847,7 +1847,7 @@ POST /v1/offers/search
|
||||
}
|
||||
</code></pre>
|
||||
<p>Часто можно столкнуться с ситуацией, когда эндпойнт просто проигнорирует наличие пустого массива <code>recipes</code> и вернёт предложения так, как будто никакого фильтра по рецепту передано не было. В нашем примере это будет означать, что приложение просто проигнорирует требование пользователя показать только напитки без молока, что мы никак не можем счесть приемлемым поведением. Поэтому ответом на такой запрос с пустым массивом в качестве параметра должна быть либо ошибка, либо пустой же массив предложений.</p>
|
||||
<h5><a href="#chapter-13-paragraph-13" id="chapter-13-paragraph-13" class="anchor">13. Валидируйте ввод</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-14" id="chapter-13-paragraph-14" class="anchor">14. Валидируйте ввод</a></h5>
|
||||
<p>Какой из вариантов действий выбрать в предыдущем примере — исключение или пустой ответ — напрямую зависит от того, что записано в вашем контракте. Если спецификация прямо предписывает, что массив <code>recipes</code> должен быть непустым, то необходимо сгенерировать исключение (иначе вы фактически нарушаете собственную спецификацию).</p>
|
||||
<p>Это верно не только в случае непустых массивов, но и любых других зафиксированных в контракте ограничений. «Тихое» исправление недопустимых значений почти никогда не имеет никакого практического смысла:</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
@ -1926,7 +1926,7 @@ POST /v1/offers/search
|
||||
strict_mode=true⮠
|
||||
disable_errors=suspicious_coordinates
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-13-paragraph-14" id="chapter-13-paragraph-14" class="anchor">14. Значения по умолчанию должны быть осмысленны</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-15" id="chapter-13-paragraph-15" class="anchor">15. Значения по умолчанию должны быть осмысленны</a></h5>
|
||||
<p>Значения по умолчанию — один из самых ваших сильных инструментов, позволяющих избежать многословности при работе с API. Однако эти умолчания должны помогать разработчикам, а не маскировать их ошибки.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>POST /v1/coffee-machines/search
|
||||
@ -1954,7 +1954,7 @@ POST /v1/offers/search
|
||||
// описание ошибки
|
||||
}
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-13-paragraph-15" id="chapter-13-paragraph-15" class="anchor">15. Ошибки должны быть информативными</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-16" id="chapter-13-paragraph-16" class="anchor">16. Ошибки должны быть информативными</a></h5>
|
||||
<p>Недостаточно просто валидировать ввод — необходимо ещё и уметь правильно описать, в чём состоит проблема. В ходе работы над интеграцией партнёры неизбежно будут допускать детские ошибки. Чем понятнее тексты сообщений, возвращаемых вашим API, тем меньше времени разработчик потратит на отладку, и тем приятнее работать с таким API.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>POST /v1/coffee-machines/search
|
||||
@ -2001,7 +2001,7 @@ POST /v1/offers/search
|
||||
}
|
||||
</code></pre>
|
||||
<p>Также хорошей практикой является указание всех допущенных ошибок, а не только первой найденной.</p>
|
||||
<h5><a href="#chapter-13-paragraph-16" id="chapter-13-paragraph-16" class="anchor">16. Всегда показывайте неразрешимые ошибки прежде разрешимых</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-17" id="chapter-13-paragraph-17" class="anchor">17. Всегда показывайте неразрешимые ошибки прежде разрешимых</a></h5>
|
||||
<p>Рассмотрим пример с заказом кофе</p>
|
||||
<pre><code>POST /v1/orders
|
||||
{
|
||||
@ -2030,7 +2030,7 @@ POST /v1/orders
|
||||
}
|
||||
</code></pre>
|
||||
<p>Какой был смысл получать новый <code>offer</code>, если заказ всё равно не может быть создан? Для пользователя это будет выглядеть как бессмысленные действия (или бессмысленное ожидание), которые всё равно завершатся ошибкой, что бы он ни делал. Да, соблюдение порядка ошибок не изменит результат — заказ всё ещё нельзя сделать — но, во-первых, пользователь потратит меньше времени (а также сделает меньше запросов к бэкенду и внесёт меньший вклад в фон ошибок) и, во-вторых, диагностика проблемы будет гораздо проще читаться.</p>
|
||||
<h5><a href="#chapter-13-paragraph-17" id="chapter-13-paragraph-17" class="anchor">17. Начинайте исправление ошибок с более глобальных</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-18" id="chapter-13-paragraph-18" class="anchor">18. Начинайте исправление ошибок с более глобальных</a></h5>
|
||||
<p>Если ошибки исправимы (т.е. пользователь может совершить какие-то действия и всё же добиться желаемого), следует в первую очередь сообщать о тех, которые потребуют более глобального изменения состояния.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>POST /v1/orders
|
||||
@ -2073,7 +2073,7 @@ POST /v1/orders
|
||||
}
|
||||
</code></pre>
|
||||
<p>Какой был смысл показывать пользователю диалог об изменившейся цене, если и с правильной ценой заказ он сделать всё равно не сможет? Пока один из его предыдущих заказов завершится и можно будет сделать следующий заказ, цену, наличие и другие параметры заказа всё равно придётся корректировать ещё раз.</p>
|
||||
<h5><a href="#chapter-13-paragraph-18" id="chapter-13-paragraph-18" class="anchor">18. Проанализируйте потенциальные взаимные блокировки</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-19" id="chapter-13-paragraph-19" class="anchor">19. Проанализируйте потенциальные взаимные блокировки</a></h5>
|
||||
<p>В сложных системах не редки ситуации, когда исправление одной ошибки приводит к возникновению другой и наоборот.</p>
|
||||
<pre><code>// Создаём заказ с платной доставкой
|
||||
POST /v1/orders
|
||||
@ -2110,7 +2110,7 @@ POST /v1/orders
|
||||
}
|
||||
</code></pre>
|
||||
<p>Легко заметить, что в этом примере нет способа разрешить ошибку в один шаг — эту ситуацию требуется предусмотреть отдельно, и либо изменить параметры расчёта (минимальная сумма заказа не учитывает скидки), либо ввести специальную ошибку для такого кейса.</p>
|
||||
<h5><a href="#chapter-13-paragraph-19" id="chapter-13-paragraph-19" class="anchor">19. Указывайте время жизни ресурсов и политики кэширования</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-20" id="chapter-13-paragraph-20" class="anchor">20. Указывайте время жизни ресурсов и политики кэширования</a></h5>
|
||||
<p>В современных системах клиент, как правило, обладает собственным состоянием и почти всегда кэширует результаты запросов — неважно, долговременно ли или в течение сессии: у каждого объекта всегда есть какое-то время автономной жизни. Поэтому желательно вносить ясность; каким образом рекомендуется кэшировать результат должно быть понятно, если не из сигнатур операций, то хотя бы из документации.</p>
|
||||
<p>Следует уточнить, что кэш мы понимаем в расширенном смысле, а именно: какое варьирование параметров операции (не только времени обращения, но и прочих переменных) следует считать достаточно близким к предыдущему запросу, чтобы можно было использовать результат из кэша?</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
@ -2151,11 +2151,11 @@ GET /v1/price?recipe=lungo⮠
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-13-paragraph-20" id="chapter-13-paragraph-20" class="anchor">20. Сохраняйте точность дробных чисел</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-21" id="chapter-13-paragraph-21" class="anchor">21. Сохраняйте точность дробных чисел</a></h5>
|
||||
<p>Там, где это позволено протоколом, дробные числа с фиксированной запятой — такие, как денежные суммы, например — должны передаваться в виде специально предназначенных для этого объектов, например, <code>Decimal</code> или аналогичных.</p>
|
||||
<p>Если в протоколе нет <code>Decimal</code>-типов (в частности, в JSON нет чисел с фиксированной запятой), следует либо привести к целому (путём домножения на указанный множитель), либо использовать строковый тип.</p>
|
||||
<p>Если конвертация в формат с плавающей запятой заведомо приводит к потере точности (например, если мы переведём 20 минут в часы в виде десятичной дроби), то следует либо предпочесть формат без потери точности (т.е. предпочесть формат <code>00:20</code> формату <code>0.333333…</code>), либо предоставить SDK работы с такими данными, либо (в крайнем случае) описать в документации принципы округления.</p>
|
||||
<h5><a href="#chapter-13-paragraph-21" id="chapter-13-paragraph-21" class="anchor">21. Все операции должны быть идемпотентны</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-22" id="chapter-13-paragraph-22" class="anchor">22. Все операции должны быть идемпотентны</a></h5>
|
||||
<p>Напомним, идемпотентность — это следующее свойство: повторный вызов той же операции с теми же параметрами не изменяет результат. Поскольку мы обсуждаем в первую очередь клиент-серверное взаимодействие, узким местом в котором является ненадежность сетевой составляющей, повтор запроса при обрыве соединения — не исключительная ситуация, а норма жизни.</p>
|
||||
<p>Там, где идемпотентность не может быть обеспечена естественным образом, необходимо добавить явный параметр — ключ идемпотентности или ревизию.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
@ -2222,12 +2222,12 @@ X-Idempotency-Token: <токен>
|
||||
<li>нельзя полагаться на то, что клиенты генерируют честные случайные токены — они могут иметь одинаковый seed рандомизатора или просто использовать слабый алгоритм или источник энтропии; при проверке токенов нужны слабые ограничения: уникальность токена должна проверяться не глобально, а только применительно к конкретному пользователю и конкретной операции;</li>
|
||||
<li>клиентские разработчики могут неправильно понимать концепцию — или генерировать новый токен на каждый перезапрос (что на самом деле неопасно, в худшем случае деградирует UX), или, напротив, использовать один токен для разнородных запросов (а вот это опасно и может привести к катастрофически последствиям; ещё одна причина имплементировать совет из предыдущего пункта!); поэтому рекомендуется написать хорошую документацию и/или клиентскую библиотеку для перезапросов.</li>
|
||||
</ul>
|
||||
<h5><a href="#chapter-13-paragraph-22" id="chapter-13-paragraph-22" class="anchor">22. Не изобретайте безопасность</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-23" id="chapter-13-paragraph-23" class="anchor">23. Не изобретайте безопасность</a></h5>
|
||||
<p>Если бы автору этой книги давали доллар каждый раз, когда ему приходилось бы имплементировать кем-то придуманный дополнительный протокол безопасности — он бы давно уже был на заслуженной пенсии. Любовь разработчиков API к подписыванию параметров запросов или сложным схемам обмена паролей на токены столь же несомненна, сколько и бессмысленна.</p>
|
||||
<p><strong>Во-первых</strong>, почти всегда процедуры, обеспечивающие безопасность той или иной операции, <em>уже разработаны</em>. Нет никакой нужды придумывать их заново, просто имплементируйте какой-то из существующих протоколов. Никакие самописные алгоритмы проверки сигнатур запросов не обеспечат вам того же уровня защиты от атаки <a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">Man-in-the-Middle</a>, как соединение по протоколу TLS с взаимной проверкой сигнатур сертификатов.</p>
|
||||
<p><strong>Во-вторых</strong>, чрезвычайно самонадеянно (и опасно) считать, что вы разбираетесь в вопросах безопасности. Новые вектора атаки появляются каждый день, и быть в курсе всех актуальных проблем — это само по себе работа на полный рабочий день. Если же вы полный рабочий день занимаетесь чем-то другим, спроектированная вами система защиты наверняка будет содержать уязвимости, о которых вы просто никогда не слышали — например, ваш алгоритм проверки паролей может быть подвержен <a href="https://en.wikipedia.org/wiki/Timing_attack">атаке по времени</a>, а веб-сервер — <a href="https://capec.mitre.org/data/definitions/105.html">атаке с разделением запросов</a>.</p>
|
||||
<p>Отдельно уточним: любые API должны предоставляться строго по протоколу TLS версии не ниже 1.2 (лучше 1.3).</p>
|
||||
<h5><a href="#chapter-13-paragraph-23" id="chapter-13-paragraph-23" class="anchor">23. Помогайте партнёрам не изобретать безопасность</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-24" id="chapter-13-paragraph-24" class="anchor">24. Помогайте партнёрам не изобретать безопасность</a></h5>
|
||||
<p>Не менее важно не только обеспечивать безопасность API как такового, но и предоставить партнёрам такие интерфейсы, которые минимизируют возможные проблемы с безопасностью на их стороне.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Позволяет партнёру задать
|
||||
@ -2285,19 +2285,19 @@ X-Dangerously-Allow-Raw-Value: true
|
||||
}
|
||||
</code></pre>
|
||||
<p>Во втором случае вы сможете централизованно экранировать небезопасный ввод и избежать тем самым SQL-инъекции. Напомним повторно, что делать это необходимо с помощью state-of-the-art инструментов, а не самописных регулярных выражений.</p>
|
||||
<h5><a href="#chapter-13-paragraph-24" id="chapter-13-paragraph-24" class="anchor">24. Используйте глобально уникальные идентификаторы</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-25" id="chapter-13-paragraph-25" class="anchor">25. Используйте глобально уникальные идентификаторы</a></h5>
|
||||
<p>Хорошим тоном при разработке API будет использование для идентификаторов сущностей глобально уникальных строк, либо семантичных (например, "lungo" для видов напитков), либо случайных (например <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)">UUID-4</a>). Это может чрезвычайно пригодиться, если вдруг придётся объединять данные из нескольких источников под одним идентификатором.</p>
|
||||
<p>Мы вообще склонны порекомендовать использование идентификаторов в urn-подобном формате, т.е. <code>urn:order:<uuid></code> (или просто <code>order:<uuid></code>), это сильно помогает с отладкой legacy-систем, где по историческим причинам есть несколько разных идентификаторов для одной и той же сущности, в таком случае неймспейсы в urn помогут быстро понять, что это за идентификатор и нет ли здесь ошибки использования.</p>
|
||||
<p>Отдельное важное следствие: <strong>не используйте инкрементальные номера как внешние идентификаторы</strong>. Помимо вышесказанного, это плохо ещё и тем, что ваши конкуренты легко смогут подсчитать, сколько у вас в системе каких сущностей и тем самым вычислить, например, точное количество заказов за каждый день наблюдений.</p>
|
||||
<h5><a href="#chapter-13-paragraph-25" id="chapter-13-paragraph-25" class="anchor">25. Предусмотрите ограничения доступа</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-26" id="chapter-13-paragraph-26" class="anchor">26. Предусмотрите ограничения доступа</a></h5>
|
||||
<p>С ростом популярности API вам неизбежно придётся внедрять технические средства защиты от недобросовестного использования — такие, как показ капчи, расстановка приманок-honeypot-ов, возврат ошибок вида «слишком много запросов», постановка прокси-защиты от DDoS перед эндпойнтами и так далее. Всё это невозможно сделать, если вы не предусмотрели такой возможности изначально, а именно — не ввели соответствующей номенклатуры ошибок и предупреждений.</p>
|
||||
<p>Вы не обязаны с самого начала такие ошибки действительно генерировать — но вы можете предусмотреть их на будущее. Например, вы можете описать ошибку <code>429 Too Many Requests</code> или перенаправление на показ капчи, но не имплементировать возврат таких ответов, пока не возникнет в этом необходимость.</p>
|
||||
<p>Отдельно необходимо уточнить, что в тех случаях, когда через API можно совершать платежи, ввод дополнительных факторов аутентификации пользователя (через TOTP, SMS или технологии типа 3D-Secure) должен быть предусмотрен обязательно.</p>
|
||||
<p><strong>NB</strong>: из этого пункта вытекает достаточно очевидное правило, которое, тем не менее, часто нарушают разработчики API — <strong>всегда разделяйте эндпойнты разных семейств API</strong>. Если вы предоставляете и серверное API, и сервисы для конечных пользователей, и виджеты для встраивания в сторонние приложения — эти API должны обслужиться с разных эндпойнтов для того, чтобы вы могли вводить разные меры безопасности (скажем, API-ключи, требование логина и капчу, соответственно).</p>
|
||||
<h5><a href="#chapter-13-paragraph-26" id="chapter-13-paragraph-26" class="anchor">26. Не предоставляйте endpoint-ов массового получения чувствительных данных</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-27" id="chapter-13-paragraph-27" class="anchor">27. Не предоставляйте endpoint-ов массового получения чувствительных данных</a></h5>
|
||||
<p>Если через API возможно получение персональных данных, номер банковских карт, переписки пользователей и прочей информации, раскрытие которой нанесёт большой ущерб пользователям, партнёрам и/или вам — методов массового получения таких данных в API быть не должно, или, по крайней мере, на них должны быть ограничения на частоту запросов, размер страницы данных, а в идеале ещё и многофакторная аутентификация.</p>
|
||||
<p>Часто разумной практикой является предоставление таких массовых выгрузок по запросу, т.е. фактически в обход API.</p>
|
||||
<h5><a href="#chapter-13-paragraph-27" id="chapter-13-paragraph-27" class="anchor">27. Локализация и интернационализация</a></h5>
|
||||
<h5><a href="#chapter-13-paragraph-28" id="chapter-13-paragraph-28" class="anchor">28. Локализация и интернационализация</a></h5>
|
||||
<p>Все эндпойнты должны принимать на вход языковые параметры (например, в виде заголовка <code>Accept-Language</code>), даже если на текущем этапе нужды в локализации нет.</p>
|
||||
<p>Важно понимать, что язык пользователя и юрисдикция, в которой пользователь находится — разные вещи. Цикл работы вашего API всегда должен хранить локацию пользователя. Либо она задаётся явно (в запросе указываются географические координаты), либо неявно (первый запрос с географическими координатами инициировал создание сессии, в которой сохранена локация) — но без локации корректная локализация невозможна. В большинстве случаев локацию допустимо редуцировать до кода страны.</p>
|
||||
<p>Дело в том, что множество параметров, потенциально влияющих на работу API, зависят не от языка, а именно от расположения пользователя. В частности, правила форматирования чисел (разделители целой и дробной частей, разделители разрядов) и дат, первый день недели, раскладка клавиатуры, система единиц измерения (которая к тому же может оказаться не десятичной!) и так далее. В некоторых ситуациях необходимо хранить две локации: та, в которой пользователь находится, и та, которую пользователь сейчас просматривает. Например, если пользователь из США планирует туристическую поездку в Европу, то цены ему желательно показывать в местной валюте, но отформатированными согласно правилам американского письма.</p>
|
||||
@ -3123,7 +3123,7 @@ GET /v1/orders/created-history⮠
|
||||
</ul>
|
||||
<p>При интеграции через webhook, партнёр указывает URL своего собственного сервера обработки сообщений, и сервер API вызывает этот эндпойнт для оповещения о произошедшем событии.</p>
|
||||
<p>Предположим, что в нашем кофейном примере партнёр располагает некоторым бэкендом, готовым принимать оповещения о новых заказах, поступивших в его кофейни, и нам нужно договориться о формате взаимодействия. Решение этой задачи декомпозируется на несколько шагов:</p>
|
||||
<h5><a href="#chapter-21-paragraph-4" id="chapter-21-paragraph-4" class="anchor">4. Договоренность о контракте</a></h5>
|
||||
<h5><a href="#chapter-21-paragraph-4" id="chapter-21-paragraph-4" class="anchor">1. Договоренность о контракте</a></h5>
|
||||
<p>В зависимости от важности партнёра для вашего бизнеса здесь возможны разные варианты:</p>
|
||||
<ul>
|
||||
<li>производитель API может реализовать возможность вызова webhook-а в формате, предложенном партнёром;</li>
|
||||
@ -3131,9 +3131,9 @@ GET /v1/orders/created-history⮠
|
||||
<li>любой промежуточный вариант.</li>
|
||||
</ul>
|
||||
<p>Важно, что в любом случае должен существовать формальный контракт (очень желательно — в виде спецификации) на форматы запросов и ответов эндпойнта-webhook-а и возникающие ошибки.</p>
|
||||
<h5><a href="#chapter-21-paragraph-5" id="chapter-21-paragraph-5" class="anchor">5. Договорённость о авторизации и аутентификации</a></h5>
|
||||
<h5><a href="#chapter-21-paragraph-5" id="chapter-21-paragraph-5" class="anchor">2. Договорённость о способах авторизации и аутентификации</a></h5>
|
||||
<p>Так как webhook-и представляют собой обратный канал взаимодействия, для него придётся разработать отдельный способ авторизации — это партнёр должен проверить, что запрос исходит от нашего бэкенда, а не наоборот. Мы повторяем здесь настоятельную рекомендацию не изобретать безопасность и использовать существующие стандартные механизмы, например, <a href="https://en.wikipedia.org/wiki/Mutual_authentication#mTLS">mTLS</a>, хотя в реальном мире с большой долей вероятности придётся использовать архаичные техники типа фиксации IP-адреса вызывающего сервера.</p>
|
||||
<h5><a href="#chapter-21-paragraph-6" id="chapter-21-paragraph-6" class="anchor">6. API для задания адреса webhook-а</a></h5>
|
||||
<h5><a href="#chapter-21-paragraph-6" id="chapter-21-paragraph-6" class="anchor">3. API для задания адреса webhook-а</a></h5>
|
||||
<p>Так как callback-эндпойнт разрабатывается партнёром, его URL нам априори неизвестен. Должен существовать интерфейс (возможно, в виде кабинета партнёра) для задания URL webhook-а (и публичных ключей авторизации).</p>
|
||||
<p><strong>Важно</strong>. К операции задания адреса callback-а нужно подходить с максимально возможной серьёзностью (очень желательно требовать второй фактор авторизации для подтверждения этой операции), поскольку, получив доступ к такой функциональности, злоумышленник может совершить множество весьма неприятных атак:</p>
|
||||
<ul>
|
||||
|
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
Binary file not shown.
@ -657,7 +657,7 @@ ul.references li p a.back-anchor {
|
||||
</ol>
|
||||
<p>Sentences “a major API version” and “new API version, containing backwards-incompatible changes” are therefore to be considered equivalent ones.</p>
|
||||
<p>It is usually (though not necessary) agreed that the last stable API release might be referenced by either a full version (e.g., <code>1.2.3</code>) or a reduced one (<code>1.2</code> or just <code>1</code>). Some systems support more sophisticated schemes of defining the desired version (for example, <code>^1.2.3</code> reads like “get the last stable API release that is backwards-compatible to the <code>1.2.3</code> version”) or additional shortcuts (for example, <code>1.2-beta</code> to refer to the last beta release of the <code>1.2</code> API version family). In this book, we will mostly use designations like <code>v1</code> (<code>v2</code>, <code>v3</code>, etc.) to denote the latest stable release of the <code>1.x.x</code> version family of an API.</p>
|
||||
<p>The practical meaning of this versioning system and the applicable policies will be discussed in more detail in <a href="#back-compat-statement">“The Backward Compatibility Problem Statement”</a> chapter.</p><div class="page-break"></div><h3><a href="#intro-terms-notation" class="anchor" id="intro-terms-notation">Chapter 6. Terms and Notation Keys</a><a href="#chapter-6" class="secondary-anchor" id="chapter-6"> </a></h3>
|
||||
<p>The practical meaning of this versioning system and the applicable policies will be discussed in more detail in “<a href="#back-compat-statement">The Backward Compatibility Problem Statement</a>” chapter.</p><div class="page-break"></div><h3><a href="#intro-terms-notation" class="anchor" id="intro-terms-notation">Chapter 6. Terms and Notation Keys</a><a href="#chapter-6" class="secondary-anchor" id="chapter-6"> </a></h3>
|
||||
<p>Software development is characterized, among other things, by the existence of many different engineering paradigms, whose adepts sometimes are quite aggressive towards other paradigms' adepts. While writing this book, we are deliberately avoiding using terms like “method,” “object,” “function,” and so on, using the neutral term “entity” instead. “Entity” means some atomic functionality unit, like class, method, object, monad, prototype (underline what you think is right).</p>
|
||||
<p>As for an entity's components, we regretfully failed to find a proper term, so we will use the words “fields” and “methods.”</p>
|
||||
<p>Most of the examples of APIs will be provided in a form of JSON-over-HTTP endpoints. This is some sort of notation that, as we see it, helps to describe concepts in the most comprehensible manner. A <code>GET /v1/orders</code> endpoint call could easily be replaced with an <code>orders.get()</code> method call, local or remote; JSON could easily be replaced with any other data format. The semantics of statements shouldn't change.</p>
|
||||
@ -1518,14 +1518,14 @@ The invalid price error is resolvable: a client could obtain a new price offer a
|
||||
<p>Proper decomposition also helps with extending and evolving an API. We'll discuss the subject in Section II.</p><div class="page-break"></div><h3><a href="#api-design-describing-interfaces" class="anchor" id="api-design-describing-interfaces">Chapter 11. Describing Final Interfaces</a><a href="#chapter-11" class="secondary-anchor" id="chapter-11"> </a></h3>
|
||||
<p>When all entities, their responsibilities, and their relations to each other are defined, we proceed to the development of the API itself. We are to describe the objects, fields, methods, and functions nomenclature in detail. In this chapter, we're giving purely practical advice on making APIs usable and understandable.</p>
|
||||
<p>An important assertion at number 0:</p>
|
||||
<h5><a href="#chapter-11-paragraph-0" id="chapter-11-paragraph-0" class="anchor">0. Rules Must Not Be Applied Unthinkingly</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-1" id="chapter-11-paragraph-1" class="anchor">0. Rules Must Not Be Applied Unthinkingly</a></h5>
|
||||
<p>Rules are just simply formulated generalizations from one's experience. They are not to be applied unconditionally, and they don't make thinking redundant. Every rule has a rational reason to exist. If your situation doesn't justify following the rule — then you shouldn't do it.</p>
|
||||
<p>For example, demanding a specification be consistent exists to help developers spare time on reading docs. If you <em>need</em> developers to read some entity's doc, it is totally rational to make its signature deliberately inconsistent.</p>
|
||||
<p>This idea applies to every concept listed below. If you get an unusable, bulky, unobvious API because you follow the rules, it's a motivation to revise the rules (or the API).</p>
|
||||
<p>It is important to understand that you can always introduce concepts of your own. For example, some frameworks willfully reject paired <code>set_entity</code> / <code>get_entity</code> methods in a favor of a single <code>entity()</code> method, with an optional argument. The crucial part is being systematic in applying the concept. If it's rendered into life, you must apply it to every single API method, or at the very least elaborate a naming rule to discern such polymorphic methods from regular ones.</p>
|
||||
<h4>Ensuring Readability and Consistency</h4>
|
||||
<p>The most important task for the API vendor is to make code written by third-party developers atop of the API easily readable and maintainable. Remember that the law of large numbers works against you: if some concept or a signature might be treated wrong, they will be inevitably treated wrong by a number of partners, and this number will be increasing with the API popularity growth.</p>
|
||||
<h5><a href="#chapter-11-paragraph-1" id="chapter-11-paragraph-1" class="anchor">1. Explicit Is Always Better Than Implicit</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-2" id="chapter-11-paragraph-2" class="anchor">2. Explicit Is Always Better Than Implicit</a></h5>
|
||||
<p>Entity name must explicitly tell what it does and what side effects to expect while using it.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>// Cancels an order
|
||||
@ -1555,7 +1555,7 @@ orders.calculateAggregatedStats({
|
||||
<p>Two important implications:</p>
|
||||
<p><strong>1.1.</strong> If the operation is modifying, it must be obvious from the signature. In particular, there might be no modifying operations using the <code>GET</code> verb.</p>
|
||||
<p><strong>1.2.</strong> If your API's nomenclature contains both synchronous and asynchronous operations, then (a)synchronicity must be apparent from signatures, <strong>or</strong> a naming convention must exist.</p>
|
||||
<h5><a href="#chapter-11-paragraph-2" id="chapter-11-paragraph-2" class="anchor">2. Specify Which Standards Are Used</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-3" id="chapter-11-paragraph-3" class="anchor">3. Specify Which Standards Are Used</a></h5>
|
||||
<p>Regretfully, humanity is unable to agree on the most trivial things, like which day starts the week, to say nothing about more sophisticated standards.</p>
|
||||
<p>So <em>always</em> specify exactly which standard is applied. Exceptions are possible if you're 100% sure that only one standard for this entity exists in the world, and every person on Earth is totally aware of it.</p>
|
||||
<p><strong>Bad</strong>: <code>"date": "11/12/2020"</code> — there are tons of date formatting standards; you can't even tell which number means the day number and which number means the month.</p>
|
||||
@ -1571,11 +1571,11 @@ or<br>
|
||||
<code>"duration": {"unit": "ms", "value": 5000}</code>.</p>
|
||||
<p>One particular implication of this rule is that money sums must <em>always</em> be accompanied by a currency code.</p>
|
||||
<p>It is also worth saying that in some areas the situation with standards is so spoiled that, whatever you do, someone got upset. A “classical” example is geographical coordinates order (latitude-longitude vs longitude-latitude). Alas, the only working method of fighting frustration there is the “Serenity Notepad” to be discussed in Section II.</p>
|
||||
<h5><a href="#chapter-11-paragraph-3" id="chapter-11-paragraph-3" class="anchor">3. Entities Must Have Concrete Names</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-4" id="chapter-11-paragraph-4" class="anchor">4. Entities Must Have Concrete Names</a></h5>
|
||||
<p>Avoid single amoeba-like words, such as “get,” “apply,” “make,” etc.</p>
|
||||
<p><strong>Bad</strong>: <code>user.get()</code> — hard to guess what is actually returned.</p>
|
||||
<p><strong>Better</strong>: <code>user.get_id()</code>.</p>
|
||||
<h5><a href="#chapter-11-paragraph-4" id="chapter-11-paragraph-4" class="anchor">4. Don't Spare the Letters</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-5" id="chapter-11-paragraph-5" class="anchor">5. Don't Spare the Letters</a></h5>
|
||||
<p>In the 21st century, there's no need to shorten entities' names.</p>
|
||||
<p><strong>Bad</strong>: <code>order.time()</code> — unclear, what time is actually returned: order creation time, order preparation time, order waiting time?…</p>
|
||||
<p><strong>Better</strong>: <code>order.get_estimated_delivery_time()</code></p>
|
||||
@ -1594,7 +1594,7 @@ strpbrk (str1, str2)
|
||||
</code></pre>
|
||||
<p>— though it's highly disputable whether this function should exist at all; a feature-rich search function would be much more convenient. Also, shortening a <code>string</code> to an <code>str</code> bears no practical sense, regretfully being a routine in many subject areas.</p>
|
||||
<p><strong>NB</strong>: sometimes field names are shortened or even omitted (e.g., a heterogenous array is passed instead of a set of named fields) to lessen the amount of traffic. In most cases, this is absolutely meaningless as usually the data is compressed at the protocol level.</p>
|
||||
<h5><a href="#chapter-11-paragraph-5" id="chapter-11-paragraph-5" class="anchor">5. Naming Implies Typing</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-6" id="chapter-11-paragraph-6" class="anchor">6. Naming Implies Typing</a></h5>
|
||||
<p>A field named <code>recipe</code> must be of a <code>Recipe</code> type. A field named <code>recipe_id</code> must contain a recipe identifier that we could find within the <code>Recipe</code> entity.</p>
|
||||
<p>Same for primitive types. Arrays must be named in a plural form or as collective nouns, e.g., <code>objects</code>, <code>children</code>. If that's impossible, better add a prefix or a postfix to avoid doubt.</p>
|
||||
<p><strong>Bad</strong>: <code>GET /news</code> — unclear whether a specific news item is returned, or a list of them.</p>
|
||||
@ -1612,7 +1612,7 @@ GET /coffee-machines/{id}/functions
|
||||
</code></pre>
|
||||
<p>The word “function” is many-valued. It could mean built-in functions, but also “a piece of code,” or a state (machine is functioning).</p>
|
||||
<p><strong>Better</strong>: <code>GET /v1/coffee-machines/{id}/builtin-functions-list</code></p>
|
||||
<h5><a href="#chapter-11-paragraph-6" id="chapter-11-paragraph-6" class="anchor">6. Matching Entities Must Have Matching Names and Behave Alike</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-7" id="chapter-11-paragraph-7" class="anchor">7. Matching Entities Must Have Matching Names and Behave Alike</a></h5>
|
||||
<p><strong>Bad</strong>: <code>begin_transition</code> / <code>stop_transition</code><br>
|
||||
— <code>begin</code> and <code>stop</code> terms don't match; developers will have to dig into the docs.</p>
|
||||
<p><strong>Better</strong>: either <code>begin_transition</code> / <code>end_transition</code> or <code>start_transition</code> / <code>stop_transition</code>.</p>
|
||||
@ -1633,7 +1633,7 @@ str_replace(needle, replace, haystack)
|
||||
<li>the first function finds the first occurrence while the second one finds them all, and there is no way to deduce that fact out of the function signatures.</li>
|
||||
</ul>
|
||||
<p>We're leaving the exercise of making these signatures better for the reader.</p>
|
||||
<h5><a href="#chapter-11-paragraph-7" id="chapter-11-paragraph-7" class="anchor">7. Avoid Double Negations</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-8" id="chapter-11-paragraph-8" class="anchor">8. Avoid Double Negations</a></h5>
|
||||
<p><strong>Bad</strong>: <code>"dont_call_me": false</code><br>
|
||||
— humans are bad at perceiving double negation; make mistakes.</p>
|
||||
<p><strong>Better</strong>: <code>"prohibit_calling": true</code> or <code>"avoid_calling": true</code><br>
|
||||
@ -1653,7 +1653,7 @@ str_replace(needle, replace, haystack)
|
||||
}
|
||||
</code></pre>
|
||||
<p>— then developers will have to evaluate the flag <code>!beans_absence && !cup_absence</code> which is equivalent to <code>!(beans_absence || cup_absence)</code> conditions, and in this transition, people tend to make mistakes. Avoiding double negations helps little, and regretfully only general advice could be given: avoid the situations when developers have to evaluate such flags.</p>
|
||||
<h5><a href="#chapter-11-paragraph-8" id="chapter-11-paragraph-8" class="anchor">8. Avoid Implicit Type Conversion</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-9" id="chapter-11-paragraph-9" class="anchor">9. Avoid Implicit Type Conversion</a></h5>
|
||||
<p>This advice is opposite to the previous one, ironically. When developing APIs you frequently need to add a new optional field with a non-empty default value. For example:</p>
|
||||
<pre><code>const orderParams = {
|
||||
contactless_delivery: false
|
||||
@ -1718,7 +1718,7 @@ PUT /v1/users/{id}
|
||||
}
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>: the contradiction with the previous rule lies in the necessity of introducing “negative” flags (the “no limit” flag), which we had to rename to <code>abolish_spending_limit</code>. Though it's a decent name for a negative flag, its semantics is still unobvious, and developers will have to read the docs. That's the way.</p>
|
||||
<h5><a href="#chapter-11-paragraph-9" id="chapter-11-paragraph-9" class="anchor">9. No Results Is a Result</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-10" id="chapter-11-paragraph-10" class="anchor">10. No Results Is a Result</a></h5>
|
||||
<p>If a server processed a request correctly and no exceptional situation occurred — there must be no error. Regretfully, an antipattern is widespread — of throwing errors when zero results are found.</p>
|
||||
<p><strong>Bad</strong></p>
|
||||
<pre><code>POST /v1/coffee-machines/search
|
||||
@ -1745,7 +1745,7 @@ PUT /v1/users/{id}
|
||||
}
|
||||
</code></pre>
|
||||
<p>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.)</p>
|
||||
<h5><a href="#chapter-11-paragraph-10" id="chapter-11-paragraph-10" class="anchor">10. Errors Must Be Informative</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-11" id="chapter-11-paragraph-11" class="anchor">11. Errors Must Be Informative</a></h5>
|
||||
<p>While writing the 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.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>POST /v1/coffee-machines/search
|
||||
@ -1793,7 +1793,7 @@ PUT /v1/users/{id}
|
||||
}
|
||||
</code></pre>
|
||||
<p>It is also a good practice to return all detectable errors at once to spare developers' time.</p>
|
||||
<h5><a href="#chapter-11-paragraph-11" id="chapter-11-paragraph-11" class="anchor">11. Maintain a Proper Error Sequence</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-12" id="chapter-11-paragraph-12" class="anchor">12. Maintain a Proper Error Sequence</a></h5>
|
||||
<p><strong>First</strong>, always return unresolvable errors before the resolvable ones:</p>
|
||||
<pre><code>POST /v1/orders
|
||||
{
|
||||
@ -1893,7 +1893,7 @@ POST /v1/orders
|
||||
<p>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.</p>
|
||||
<h4>Developing Machine-Readable Interfaces</h4>
|
||||
<p>In pursuit of API clarity for humans, we frequently forget that it's not developers themselves who interact with the endpoints, but the code they've written. Many concepts that work well with user interfaces are badly suited for the program ones: specifically, developers can't make decisions based on textual information, and they can't “refresh” the state in case of some confusing situation.</p>
|
||||
<h5><a href="#chapter-11-paragraph-12" id="chapter-11-paragraph-12" class="anchor">12. The State of the System Must Be Observable by Clients</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-13" id="chapter-11-paragraph-13" class="anchor">13. The State of the System Must Be Observable by Clients</a></h5>
|
||||
<p>Sometimes, program systems provide interfaces that do not expose to the clients all the data on what is now being executed on the user's
|
||||
behalf, specifically — which operations are running and what their statuses are.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
@ -1958,7 +1958,7 @@ GET /v1/users/{id}/orders
|
||||
]
|
||||
}
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-11-paragraph-13" id="chapter-11-paragraph-13" class="anchor">13. Specify Caching Policies and Lifespans of Resources</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-14" id="chapter-11-paragraph-14" class="anchor">14. Specify Caching Policies and Lifespans of Resources</a></h5>
|
||||
<p>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.</p>
|
||||
<p>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?</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
@ -2000,7 +2000,7 @@ GET /price?recipe=lungo⮠
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-11-paragraph-14" id="chapter-11-paragraph-14" class="anchor">14. Pagination, Filtration, and Cursors</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-15" id="chapter-11-paragraph-15" class="anchor">15. Pagination, Filtration, and Cursors</a></h5>
|
||||
<p>Any endpoints returning data collections must be paginated. No exclusions exist.</p>
|
||||
<p>Any paginated endpoint must provide an interface to iterate over all the data.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
@ -2113,11 +2113,11 @@ GET /v1/records/modified/list⮠
|
||||
<p>This scheme's downsides are the necessity to create separate indexed event storage, and the multiplication of data items, since for a single record many events might exist.</p>
|
||||
<h4>Ensuring the Technical Quality of APIs</h4>
|
||||
<p>Fine APIs must not only solve developers' and end users' problems but also ensure the quality of the solution, i.e., do not contain logical and technical mistakes (and do not provoke developers to make them), save computational resources, and in general implement the best practices applicable to the subject area.</p>
|
||||
<h5><a href="#chapter-11-paragraph-15" id="chapter-11-paragraph-15" class="anchor">15. Keep the Precision of Fractional Numbers Intact</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-16" id="chapter-11-paragraph-16" class="anchor">16. Keep the Precision of Fractional Numbers Intact</a></h5>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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 <code>00:20</code> instead of <code>0.33333…</code>) or to provide an SDK to work with this data, or as a last resort describe the rounding principles in the documentation.</p>
|
||||
<h5><a href="#chapter-11-paragraph-16" id="chapter-11-paragraph-16" class="anchor">16. All API Operations Must Be Idempotent</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-17" id="chapter-11-paragraph-17" class="anchor">17. All API Operations Must Be Idempotent</a></h5>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
@ -2184,7 +2184,7 @@ X-Idempotency-Token: <token>
|
||||
<li>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;</li>
|
||||
<li>clients tend to misunderstand the concept and either generate new tokens each time they repeat the request (which deteriorates the UX, but otherwise healthy) or conversely use one token in several requests (not healthy at all and could lead to catastrophic disasters; another reason to implement the suggestion in the previous clause); writing detailed doc and/or client library is highly recommended.</li>
|
||||
</ul>
|
||||
<h5><a href="#chapter-11-paragraph-17" id="chapter-11-paragraph-17" class="anchor">17. Avoid Non-Atomic Operations</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-18" id="chapter-11-paragraph-18" class="anchor">18. Avoid Non-Atomic Operations</a></h5>
|
||||
<p>There is a common problem with implementing the changes list approach: what to do if some changes were successfully applied, while others are not? The rule is simple: if you may ensure the atomicity (i.e., either apply all changes or none of them) — do it.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>// Returns a list of recipes
|
||||
@ -2318,15 +2318,15 @@ api.getRecipes();
|
||||
<p>To the client, everything looks normal: changes were applied, and the last response got is always actual. But the resource state after the first request was inherently different from the resource state after the second one, which contradicts the very definition of “idempotency.”</p>
|
||||
<p>It would be more correct if the server did nothing upon getting the second request with the same idempotency token, and returned the same status list breakdown. But it implies that storing these breakdowns must be implemented.</p>
|
||||
<p>Just in case: nested operations must be idempotent themselves. If they are not, separate idempotency tokens must be generated for each nested operation.</p>
|
||||
<h5><a href="#chapter-11-paragraph-18" id="chapter-11-paragraph-18" class="anchor">18. Don't Invent Security Practices</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-19" id="chapter-11-paragraph-19" class="anchor">19. Don't Invent Security Practices</a></h5>
|
||||
<p>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 already retire. The API developers' passion for signing request parameters or introducing complex schemes of exchanging passwords for tokens is as obvious as meaningless.</p>
|
||||
<p><strong>First</strong>, almost all security-enhancing procedures for every kind of operation <em>are already invented</em>. 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 <a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">Man-in-the-Middle attack</a> as a TLS connection with mutual certificate pinning.</p>
|
||||
<p><strong>Second</strong>, 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 <a href="https://en.wikipedia.org/wiki/Timing_attack">timing attack</a>, and your webserver, to the <a href="https://capec.mitre.org/data/definitions/105.html">request splitting attack</a>.</p>
|
||||
<h5><a href="#chapter-11-paragraph-19" id="chapter-11-paragraph-19" class="anchor">19. Explicitly Declare Technical Restrictions</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-20" id="chapter-11-paragraph-20" class="anchor">20. Explicitly Declare Technical Restrictions</a></h5>
|
||||
<p>Every field in your API comes with restrictions: the maximum allowed text length, the size of attached documents, the allowed ranges for numeric values, etc. Often, describing those limits is neglected by API developers — either because they consider it obvious, or because they simply don't know the boundaries themselves. This is of course an antipattern: not knowing what are the limits automatically implies that partners' code might stop working at any moment because of the reasons they don't control.</p>
|
||||
<p>Therefore, first, declare the boundaries for every field in the API without any exceptions, and, second, generate proper machine-readable errors describing which exact boundary was violated should such a violation occur.</p>
|
||||
<p>The same reasoning applies to quotas as well: partners must have access to the statistics on which part of the quota they have already used, and the errors in the case of exceeding quotas must be informative.</p>
|
||||
<h5><a href="#chapter-11-paragraph-20" id="chapter-11-paragraph-20" class="anchor">20. Count the Amount of Traffic</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-21" id="chapter-11-paragraph-21" class="anchor">21. Count the Amount of Traffic</a></h5>
|
||||
<p>Nowadays the amount of traffic is rarely taken into account — the Internet connection is considered unlimited almost universally. However, it's still not entirely unlimited: with some degree of carelessness, it's always possible to design a system generating the amount of traffic that is uncomfortable even for modern networks.</p>
|
||||
<p>There are three obvious reasons for inflating network traffic:</p>
|
||||
<ul>
|
||||
@ -2357,7 +2357,7 @@ api.getRecipes();
|
||||
</li>
|
||||
</ul>
|
||||
<p>As a useful exercise, try modeling the typical lifecycle of a partner's app's main functionality (for example, making a single order) to count the number of requests and the amount of traffic that it takes.</p>
|
||||
<h5><a href="#chapter-11-paragraph-21" id="chapter-11-paragraph-21" class="anchor">21. Avoid Implicit Partial Updates</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-22" id="chapter-11-paragraph-22" class="anchor">22. Avoid Implicit Partial Updates</a></h5>
|
||||
<p>One of the most common API design antipatterns is an attempt to spare something on detailed state change descriptions.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>// Creates an order comprising
|
||||
@ -2483,19 +2483,19 @@ X-Idempotency-Token: <idempotency token>
|
||||
<p>This approach is much harder to implement, but it's the only viable method to implement collaborative editing since it explicitly reflects what a user was actually doing with entity representation. With data exposed in such a format, you might actually implement offline editing, when user changes are accumulated and then sent at once, while the server automatically resolves conflicts by “rebasing” the changes.</p>
|
||||
<h4>Ensuring API Product Quality</h4>
|
||||
<p>Apart from the technological limitations, any real API will soon face the imperfection of the surrounding reality. Of course, any one of us would prefer living in the world of pink unicorns, free of piles of legacy code, evil-doers, national conflicts, and competitors' scheming. Fortunately or not, we live in the real world, and API vendors have to mind all of those while developing the API.</p>
|
||||
<h5><a href="#chapter-11-paragraph-22" id="chapter-11-paragraph-22" class="anchor">22. Use Globally Unique Identifiers</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-23" id="chapter-11-paragraph-23" class="anchor">23. Use Globally Unique Identifiers</a></h5>
|
||||
<p>It's considered a good form to use globally unique strings as entity identifiers, either semantic (e.g., "lungo" for beverage types) or random ones (e.g., <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)">UUID-4</a>). It might turn out to be extremely useful if you need to merge data from several sources under a single identifier.</p>
|
||||
<p>In general, we tend to advise using urn-like identifiers, e.g., <code>urn:order:<uuid></code> (or just <code>order:<uuid></code>). 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.</p>
|
||||
<p>One important implication: <strong>never use increasing numbers as external identifiers</strong>. 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.</p>
|
||||
<p><strong>NB</strong>: in this book, we often use short identifiers like "123" in code examples; that's for the convenience of reading the book on small screens. Do not replicate this practice in a real-world API.</p>
|
||||
<h5><a href="#chapter-11-paragraph-23" id="chapter-11-paragraph-23" class="anchor">23. Stipulate Future Restrictions</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-24" id="chapter-11-paragraph-24" class="anchor">24. Stipulate Future Restrictions</a></h5>
|
||||
<p>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.</p>
|
||||
<p>You are not obliged to actually generate those exceptions, but you might stipulate this possibility in the Terms of Service (ToS). For example, you might describe the <code>429 Too Many Requests</code> error or captcha redirect but implement the functionality when it's actually needed.</p>
|
||||
<p>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.</p>
|
||||
<h5><a href="#chapter-11-paragraph-24" id="chapter-11-paragraph-24" class="anchor">24. No Bulk Access to Sensitive Data</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-25" id="chapter-11-paragraph-25" class="anchor">25. No Bulk Access to Sensitive Data</a></h5>
|
||||
<p>If it's possible to get through 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 you — there must be <em>no</em> methods of bulk getting the data, or at least there must be rate limiters, page size restrictions, and, ideally, multi-factored authentication in front of them.</p>
|
||||
<p>Often, making such offloads on an ad-hoc basis, i.e., bypassing the API, is a reasonable practice.</p>
|
||||
<h5><a href="#chapter-11-paragraph-25" id="chapter-11-paragraph-25" class="anchor">25. Localization and Internationalization</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-26" id="chapter-11-paragraph-26" class="anchor">26. Localization and Internationalization</a></h5>
|
||||
<p>All endpoints must accept language parameters (for example, in a form of the <code>Accept-Language</code> header), even if they are not being used currently.</p>
|
||||
<p>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.</p>
|
||||
<p>The thing is that lots of parameters potentially affecting data formats depend not on language, but on a 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.</p>
|
||||
@ -2764,7 +2764,7 @@ POST /v1/runtimes/{id}/terminate
|
||||
<li>finally, stating all three numbers (major version, minor version, and patch) allows for fixing a concrete API release with all its specificities (and errors), which — theoretically — means that the integration will remain operable till this version is physically available.</li>
|
||||
</ul>
|
||||
<p>Of course, preserving minor versions infinitely isn't possible (partly because of security and compliance issues that tend to pile up). However, providing such access for a reasonable period of time is rather a hygienic norm for popular APIs.</p>
|
||||
<p><strong>NB</strong>. Sometimes to defend the single accessible API version concept, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backward compatibility as it might be relying on some un-versioned services (for example, some data in the DB that are shared between all the API versions). We, however, consider this an additional reason to isolate such dependencies (see <a href="#back-compat-serenity-notepad">“The Serenity Notepad”</a> chapter) as it means that changes to these subsystems might lead to the inoperability of the API.</p><div class="page-break"></div><h3><a href="#back-compat-iceberg-waterline" class="anchor" id="back-compat-iceberg-waterline">Chapter 14. On the Waterline of the Iceberg</a><a href="#chapter-14" class="secondary-anchor" id="chapter-14"> </a></h3>
|
||||
<p><strong>NB</strong>. Sometimes to defend the single accessible API version concept, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backward compatibility as it might be relying on some un-versioned services (for example, some data in the DB that are shared between all the API versions). We, however, consider this an additional reason to isolate such dependencies (see “<a href="#back-compat-serenity-notepad">The Serenity Notepad</a>” chapter) as it means that changes to these subsystems might lead to the inoperability of the API.</p><div class="page-break"></div><h3><a href="#back-compat-iceberg-waterline" class="anchor" id="back-compat-iceberg-waterline">Chapter 14. On the Waterline of the Iceberg</a><a href="#chapter-14" class="secondary-anchor" id="chapter-14"> </a></h3>
|
||||
<p>Before we start talking about the extensible API design, we should discuss the hygienic minimum. A huge number of problems would have never happened if API vendors had paid more attention to marking their area of responsibility.</p>
|
||||
<h4>Provide a Minimal Amount of Functionality</h4>
|
||||
<p>At any moment in its lifetime, your API is like an iceberg: it comprises an observable (i.e., documented) part and a hidden one, undocumented. If the API is designed properly, these two parts correspond to each other just like the above-water and under-water parts of a real iceberg do, i.e. one to ten. Why so? Because of two obvious reasons.</p>
|
||||
@ -2813,7 +2813,7 @@ if (status) {
|
||||
}
|
||||
</code></pre>
|
||||
<p>We presume we may skip the explanations why such code must never be written under any circumstances. If you're really providing a non-strictly consistent API, then either the <code>createOrder</code> operation must be asynchronous and return the result when all replicas are synchronized, or the retry policy must be hidden inside the <code>getStatus</code> operation implementation.</p>
|
||||
<p>If you failed to describe the eventual consistency in the first place, then you simply couldn't make these changes in the API. You will effectively break backward compatibility, which will lead to huge problems with your customers' apps, intensified by the fact they can't be simply reproduced by QA engineers.</p>
|
||||
<p>If you failed to describe the eventual consistency in the first place, then you simply couldn't make these changes in the API. You will effectively break backward compatibility which will lead to huge problems with your customers' apps, intensified by the fact they can't be simply reproduced by QA engineers.</p>
|
||||
<p><strong>Example #2</strong>. Take a look at the following code:</p>
|
||||
<pre><code>let resolve;
|
||||
let promise = new Promise(
|
||||
@ -2974,7 +2974,7 @@ POST /v1/recipes
|
||||
}
|
||||
</code></pre>
|
||||
<p>At first glance, again, it looks like a reasonably simple interface, explicitly decomposed into abstraction levels. But let us imagine the future — what would happen with this interface when our system evolves further?</p>
|
||||
<p>The first problem is obvious to those who read the <a href="#api-design-describing-interfaces">“Describing Final Interfaces”</a> chapter thoroughly: product properties must be localized. That will lead us to the first change:</p>
|
||||
<p>The first problem is obvious to those who read the “<a href="#api-design-describing-interfaces">Describing Final Interfaces</a>” chapter thoroughly: product properties must be localized. That will lead us to the first change:</p>
|
||||
<pre><code>"product_properties": {
|
||||
// "l10n" is the standard abbreviation
|
||||
// for "localization"
|
||||
@ -3110,7 +3110,7 @@ PUT /formatters/volume/ru/US
|
||||
…
|
||||
}
|
||||
</code></pre>
|
||||
<p>We should also note that providing a newly created entity identifier by the requesting side isn't exactly the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, we're risking getting lots of collisions on recipe names used by different partners, so we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the <a href="#api-design-describing-interfaces">“Describing Final Interfaces”</a> chapter.</p>
|
||||
<p>We should also note that providing a newly created entity identifier by the requesting side isn't exactly the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, we're risking getting lots of collisions on recipe names used by different partners, so we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the “<a href="#api-design-describing-interfaces">Describing Final Interfaces</a>” chapter.</p>
|
||||
<pre><code>POST /v1/recipes/custom
|
||||
{
|
||||
// The first part of the composite
|
||||
@ -3127,7 +3127,7 @@ PUT /formatters/volume/ru/US
|
||||
}
|
||||
</code></pre>
|
||||
<p>Also note that this format allows us to maintain an important extensibility point: different partners might have both shared and isolated namespaces. Furthermore, we might introduce special namespaces (like <code>common</code>, for example) to allow editing standard recipes (and thus organizing our own recipes backoffice).</p>
|
||||
<p><strong>NB</strong>: a mindful reader might have noted that this technique was already used in our API study much earlier in the <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> chapter with regards to the “program” and “program run” entities. Indeed, we might do it without the <code>program-matcher</code> endpoint and make it this way:</p>
|
||||
<p><strong>NB</strong>: a mindful reader might have noted that this technique was already used in our API study much earlier in the “<a href="#api-design-separating-abstractions">Separating Abstraction Levels</a>” chapter with regards to the “program” and “program run” entities. Indeed, we might do it without the <code>program-matcher</code> endpoint and make it this way:</p>
|
||||
<pre><code>GET /v1/recipes/{id}/run-data/{api_type}
|
||||
→
|
||||
{ /* A description, how to
|
||||
@ -3141,7 +3141,7 @@ PUT /formatters/volume/ru/US
|
||||
<li>depending on the API type, run some specific commands.</li>
|
||||
</ul>
|
||||
<p>Obviously, such an interface is absolutely unacceptable, simply because in the majority of use cases developers don't care at all, which API type the specific coffee machine runs. To avoid the necessity of introducing such bad interfaces we created a new “program” entity, which constitutes merely a context identifier, just like a “recipe” entity does. A <code>program_run_id</code> entity is also organized in this manner, it also possesses no specific properties, being <em>just</em> a program run identifier.</p><div class="page-break"></div><h3><a href="#back-compat-weak-coupling" class="anchor" id="back-compat-weak-coupling">Chapter 17. Weak Coupling</a><a href="#chapter-17" class="secondary-anchor" id="chapter-17"> </a></h3>
|
||||
<p>In the previous chapter, we've demonstrated how breaking strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. But let us return to the question we have previously mentioned in the <a href="#back-compat-abstracting-extending">“Extending through Abstracting”</a> chapter: how should we parametrize the order preparation process implemented via a third-party API? In other words, what <em>is</em> the <code>order_execution_endpoint</code> required in the API type registration handler?</p>
|
||||
<p>In the previous chapter, we've demonstrated how breaking strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. But let us return to the question we have previously mentioned in the “<a href="#back-compat-abstracting-extending">Extending through Abstracting</a>” chapter: how should we parametrize the order preparation process implemented via a third-party API? In other words, what <em>is</em> the <code>order_execution_endpoint</code> required in the API type registration handler?</p>
|
||||
<pre><code>PUT /v1/api-types/{api_type}
|
||||
{
|
||||
…
|
||||
@ -3189,7 +3189,7 @@ PUT /formatters/volume/ru/US
|
||||
<li>the higher-level program API level doesn't actually know how the execution of its commands works; it formulates the tasks at its own level of understanding: brew this recipe, send user's requests to a partner, allow the user to collect their order;</li>
|
||||
<li>the underlying program execution API level doesn't care what other same-level implementations exist; it just interprets those parts of the task that make sense to it.</li>
|
||||
</ul>
|
||||
<p>If we take a look at the principles described in the previous chapter, we would find that this principle was already formulated: we need to describe <em>informational contexts</em> at every abstraction level and design a mechanism to translate them between levels. Furthermore, in a more general sense, we formulated it as early as in “The Data Flow” paragraph of the <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> chapter.</p>
|
||||
<p>If we take a look at the principles described in the previous chapter, we would find that this principle was already formulated: we need to describe <em>informational contexts</em> at every abstraction level and design a mechanism to translate them between levels. Furthermore, in a more general sense, we formulated it as early as in “The Data Flow” paragraph of the “<a href="#api-design-separating-abstractions">Separating Abstraction Levels</a>” chapter.</p>
|
||||
<p>In our case we need to implement the following mechanisms:</p>
|
||||
<ul>
|
||||
<li>running a program creates a corresponding context comprising all the essential parameters;</li>
|
||||
@ -3331,7 +3331,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<h4>Delegate!</h4>
|
||||
<p>From what was said, one more important conclusion follows: doing a real job, i.e., implementing some concrete actions (making coffee, in our case) should be delegated to the lower levels of the abstraction hierarchy. If the upper levels try to prescribe some specific implementation algorithms, then (as we have demonstrated on the <code>order_execution_endpoint</code> example) we will soon face a situation of inconsistent methods and interaction protocols nomenclature, most of which have no specific meaning when we talk about some specific hardware context.</p>
|
||||
<p>Contrariwise, applying the paradigm of concretizing the contexts at each new abstraction level, we will eventually fall into the bunny hole deep enough to have nothing to concretize: the context itself unambiguously matches the functionality we can programmatically control. And at that level, we must stop detailing contexts further, and just realize the algorithms needed. It's worth mentioning that the abstraction deepness for different underlying platforms might vary.</p>
|
||||
<p><strong>NB</strong>. In the <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> chapter we have illustrated exactly this: when we speak about the first coffee machine API type, there is no need to extend the tree of abstractions further than running programs, but with the second API type, we need one more intermediary abstraction level, namely the runtimes API.</p><div class="page-break"></div><h3><a href="#back-compat-universal-interfaces" class="anchor" id="back-compat-universal-interfaces">Chapter 18. Interfaces as a Universal Pattern</a><a href="#chapter-18" class="secondary-anchor" id="chapter-18"> </a></h3>
|
||||
<p><strong>NB</strong>. In the “<a href="#api-design-separating-abstractions">Separating Abstraction Levels</a>” chapter we have illustrated exactly this: when we speak about the first coffee machine API type, there is no need to extend the tree of abstractions further than running programs, but with the second API type, we need one more intermediary abstraction level, namely the runtimes API.</p><div class="page-break"></div><h3><a href="#back-compat-universal-interfaces" class="anchor" id="back-compat-universal-interfaces">Chapter 18. Interfaces as a Universal Pattern</a><a href="#chapter-18" class="secondary-anchor" id="chapter-18"> </a></h3>
|
||||
<p>Let us summarize what we have written in the three previous chapters:</p>
|
||||
<ol>
|
||||
<li>Extending API functionality is implemented through abstracting: the entity nomenclature is to be reinterpreted so that existing methods become partial (ideally — the most frequent) simplified cases to more general functionality.</li>
|
||||
@ -3355,7 +3355,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<p>So our interface (let us call it <code>ISearchResult</code>) is actually a composition of two other interfaces: <code>IOrderParameters</code> (an entity that allows for creating an order) and <code>ISearchItemViewParameters</code> (some abstract representation of the search result in the UI). This interface split should automatically lead us to additional questions:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>How will we couple the former and the latter? Obviously, these two sub-interfaces are related: the machine-readable price must match the human-readable one, for example. This will naturally lead us to the “formatter” concept described in the <a href="#back-compat-strong-coupling">“Strong Coupling and Related Problems”</a> chapter.</p>
|
||||
<p>How will we couple the former and the latter? Obviously, these two sub-interfaces are related: the machine-readable price must match the human-readable one, for example. This will naturally lead us to the “formatter” concept described in the “<a href="#back-compat-strong-coupling">Strong Coupling and Related Problems</a>” chapter.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>And what is the “abstract representation of the search result in the UI”? Do we have other kinds of search, should the <code>ISearchItemViewParameters</code> interface be a subtype of some even more general interface, or maybe a composition of several such ones?</p>
|
||||
@ -3378,7 +3378,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<li>The interface abstraction principle must be tested either. In theory, you might have considered each entity as an implementation of some interface; in practice, it might happen that you have forgotten something and alternative implementations aren't actually possible. For testing purposes, it's highly desirable to have an alternative realization, even a provisional one, for every interface.</li>
|
||||
</ol>
|
||||
<h5><a href="#chapter-19-paragraph-3" id="chapter-19-paragraph-3" class="anchor">3. Isolate the Dependencies</a></h5>
|
||||
<p>In the case of a gateway API that provides access to some underlying API or aggregates several APIs behind a single façade, there is a strong temptation to proxy the original interface as is, thus not introducing any changes to it and making life much simpler by sparing an effort needed to implement the weak-coupled interaction between services. For example, while developing program execution interfaces as described in the <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> chapter we might have taken the existing first-kind coffee-machine API as a role model and provided it in our API by just proxying the requests and responses as is. Doing so is highly undesirable because of several reasons:</p>
|
||||
<p>In the case of a gateway API that provides access to some underlying API or aggregates several APIs behind a single façade, there is a strong temptation to proxy the original interface as is, thus not introducing any changes to it and making life much simpler by sparing an effort needed to implement the weak-coupled interaction between services. For example, while developing program execution interfaces as described in the “<a href="#api-design-separating-abstractions">Separating Abstraction Levels</a>” chapter we might have taken the existing first-kind coffee-machine API as a role model and provided it in our API by just proxying the requests and responses as is. Doing so is highly undesirable because of several reasons:</p>
|
||||
<ul>
|
||||
<li>usually, you have no guarantees that the partner will maintain backward compatibility or at least keep new versions more or less conceptually akin to the older ones;</li>
|
||||
<li>any partner's problem will automatically ricochet into your customers.</li>
|
||||
@ -3399,7 +3399,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<p>There is an antipattern that occurs frequently: API developers use some internal closed implementations of some methods which exist in the public API. It happens because of two reasons:</p>
|
||||
<ul>
|
||||
<li>often the public API is just an addition to the existing specialized software, and the functionality, exposed via the API, isn't being ported back to the closed part of the project, or the public API developers simply don't know the corresponding internal functionality exists;</li>
|
||||
<li>in the course of extending the API, some interfaces become abstract, but the existing functionality isn't affected; imagine that while implementing the <code>PUT /formatters</code> interface described in the <a href="#back-compat-strong-coupling">“Strong Coupling and Related Problems”</a> chapter API developers have created a new, more general version of the volume formatter but hasn't changed the implementation of the existing one, so it continues working for pre-existing languages.</li>
|
||||
<li>in the course of extending the API, some interfaces become abstract, but the existing functionality isn't affected; imagine that while implementing the <code>PUT /formatters</code> interface described in the “<a href="#back-compat-strong-coupling">Strong Coupling and Related Problems</a>” chapter API developers have created a new, more general version of the volume formatter but hasn't changed the implementation of the existing one, so it continues working for pre-existing languages.</li>
|
||||
</ul>
|
||||
<p>There are obvious local problems with this approach (like the inconsistency in functions' behavior, or the bugs which were not found while testing the code), but also a bigger one: your API might be simply unusable if a developer tries any non-mainstream approach, because of performance issues, bugs, instability, etc., as the API developers themselves never tried to use this public interface for anything important.</p>
|
||||
<p><strong>NB</strong>. The perfect example of avoiding this anti-pattern is the development of compilers; usually, the next compiler's version is compiled with the previous compiler's version.</p>
|
||||
@ -3471,7 +3471,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<li>internal customers employ quite a specific technological stack, and the API is poorly optimized to work with other programming languages / operating systems / frameworks;</li>
|
||||
<li>for external customers, the learning curve will be pretty flat as they can't take a look at the source code or talk to the API developers directly, unlike internal customers that are much more familiar with the API concepts;</li>
|
||||
<li>documentation often covers only some subset of use cases needed by internal customers;</li>
|
||||
<li>the API services ecosystem which we will describe in <a href="#api-product-range">“The API Services Range”</a> chapter usually doesn't exist.</li>
|
||||
<li>the API services ecosystem which we will describe in “<a href="#api-product-range">The API Services Range</a>” chapter usually doesn't exist.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Any resources spent are directed to covering internal customer needs first. It means the following:
|
||||
@ -3528,7 +3528,7 @@ ProgramContext.dispatch = (action) => {
|
||||
</ul>
|
||||
<p>As both approaches are still heuristic, the API product vision is inevitably fuzzy, and it's rather normal: if you could have got a full and clear understanding of what end-user products might be developed on top of your API, you might have developed them on your own behalf, skipping intermediary agents. It is also important to keep in mind that many APIs pass the “terraforming” stage (see the previous chapter) thus preparing the ground for new markets and new types of services — so your idealistic vision of a nearby future where delivering freshly brewed coffee by drones will be a norm of life is to be refined and clarified while new companies providing new kinds of services are coming to the market. (Which in its turn will make an impact on the monetization model: detailing the countenance of the forthcoming will make your abstract KPIs and theoretical benefits of having an API more and more concrete.)</p>
|
||||
<p>The same fuzziness should be kept in mind while making interviews and getting feedback. Software engineers will mainly report the problems they've got with the technical integrations, and rarely speak of business-related issues; meanwhile, business owners care little about the inconvenience of writing code. Both will have some knowledge regarding the end users' problems, but it's usually limited to the market segment the partner operates on.</p>
|
||||
<p>If you do have an access to end users' actions monitoring (see <a href="#api-product-kpi">“The API Key Performance Indicators”</a> chapter), then you might try to analyze the typical user behavior through these logs and understand how users interact with the partners' applications. But you will need to make this analysis on a per-application basis and try to clusterize the most common scenarios.</p>
|
||||
<p>If you do have an access to end users' actions monitoring (see “<a href="#api-product-kpi">The API Key Performance Indicators</a>” chapter), then you might try to analyze the typical user behavior through these logs and understand how users interact with the partners' applications. But you will need to make this analysis on a per-application basis and try to clusterize the most common scenarios.</p>
|
||||
<h4>Checking Product Hypotheses</h4>
|
||||
<p>Apart from the general complexity of formulating the product vision, there are also tactical issues with checking product hypotheses. “The Holy Grail” of product management — that is, creating a cheap (in terms of resource spent) minimal viable product (MVP) — is normally unavailable for an API product manager. The thing is that you can't easily <em>test</em> the solution even if you managed to develop an API MVP: to do so, partners are to <em>develop some code</em>, i.e., invest their money; and if the outcome of the experiment is negative (i.e., the further development looks unpromising), this money will be wasted. Of course, partners will be a little bit skeptical towards such proposals. Thus a “cheap” MVP should include either the compensation for partners' expenses or the budget to develop a reference implementation (i.e., a complementary application that is created to support the API MVP).</p>
|
||||
<p>You might partially solve the problem by making some third-party company release the MVP (for example, in a form of an open-source module published in some developer's personal repository) but then you will struggle with hypothesis validation issues as such modules might easily go unnoticed.</p>
|
||||
@ -3605,7 +3605,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<li>A huge share of customers' inquiries to your customer support service will be generated by the first category of developers: it's much harder for amateurs or beginners to find answers to their questions by themselves, and they will address them to you.</li>
|
||||
<li>At the same time, the second category is much more sensitive to the quality of both the product and customer support, and fulfilling their requests might be non-trivial.</li>
|
||||
</ul>
|
||||
<p>Finally, it's almost impossible in a course of a single product to create an API that will fit well both amateur and professional developers: the former need the maximum simplicity of implementing basic use cases, while the latter seek the ability to adapt the API to match technological stack and development paradigms, and the problems they solve usually require deep customization. We will discuss the matter in <a href="#api-product-range">“The API Services Range”</a> chapter.</p><div class="page-break"></div><h3><a href="#api-product-business-comms" class="anchor" id="api-product-business-comms">Chapter 24. Communicating with Business Owners</a><a href="#chapter-24" class="secondary-anchor" id="chapter-24"> </a></h3>
|
||||
<p>Finally, it's almost impossible in a course of a single product to create an API that will fit well both amateur and professional developers: the former need the maximum simplicity of implementing basic use cases, while the latter seek the ability to adapt the API to match technological stack and development paradigms, and the problems they solve usually require deep customization. We will discuss the matter in “<a href="#api-product-range">The API Services Range</a>” chapter.</p><div class="page-break"></div><h3><a href="#api-product-business-comms" class="anchor" id="api-product-business-comms">Chapter 24. Communicating with Business Owners</a><a href="#chapter-24" class="secondary-anchor" id="chapter-24"> </a></h3>
|
||||
<p>The basics of interacting with business partners are to some extent paradoxically contrary to the basics of communicating with developers:</p>
|
||||
<ul>
|
||||
<li>on one hand, partners are much more loyal and sometimes even enthusiastic regarding opportunities you offer (especially free ones);</li>
|
||||
@ -3628,7 +3628,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<h4>Vertical Scaling of API Services</h4>
|
||||
<p>However, frequently it makes sense to provide several API services manipulating the same functionality. Let us remind you that there are two kinds of developers: professional ones that seek extensive customization capabilities (as they usually work in big IT companies that have a specific mindset towards integrations), and semi-professionals who just need the gentlest possible learning curve. The only way to cover the needs of both categories is to develop a range of products with different entry thresholds and requirements for developers' professional level. We might name several API sub-types, ordered from the most technically demanding to less complex ones.</p>
|
||||
<ol>
|
||||
<li>The most advanced level is that of physical APIs and the abstractions on top of them. [In our coffee example, the collection of entities describing working with APIs of physical coffee machines, see the <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> and the <a href="#back-compat-weak-coupling">“Weak Coupling”</a> chapters.]</li>
|
||||
<li>The most advanced level is that of physical APIs and the abstractions on top of them. [In our coffee example, the collection of entities describing working with APIs of physical coffee machines, see the “<a href="#api-design-separating-abstractions">Separating Abstraction Levels</a>” and the “<a href="#back-compat-weak-coupling">Weak Coupling</a>” chapters.]</li>
|
||||
<li>The basic level of working with product entities via formal interfaces. [In our study example, that will be HTTP API for making orders.]</li>
|
||||
<li>Working with product entities might be simplified if SDKs are provided for some popular platforms that tailor API concepts according to the paradigms of those platforms (for those developers who are proficient with specific platforms only that will save a lot of effort on dealing with formal protocols and interfaces).</li>
|
||||
<li>The next simplification step is providing services for code generation. In this service, developers choose one of the pre-built integration templates, customize some options, and got a ready-to-use piece of code that might be simply copy-pasted into the application code (and might be additionally customized by adding some level 1-3 code). This approach is sometimes called “point-and-click programming.” [In the case of our coffee API, an example of such a service might have a form or screen editor for a developer to place UI elements and get the working application code.]</li>
|
||||
@ -3643,7 +3643,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<li>Code generation makes it possible to manipulate the desired form of integrations. For example, if our KPI is a number of searches performed through the API, we might alter the generated code so it will show the search panel in the most convenient position in the app; as partners using code-generation services rarely make any changes in the resulting code, and this will help us in reaching the goal.</li>
|
||||
<li>Finally, ready-to-use components and widgets are under your full control, and you might experiment with functionality exposed through them in partners' applications just as if it was your own service. (However, it doesn't automatically mean that you might draw some profits from having this control; for example, if you're allowing inserting pictures by their direct URL, your control over this integration is rather negligible, so it's generally better to provide those kinds of integration that allow having more control over the functionality in partners' apps.)</li>
|
||||
</ol>
|
||||
<p><strong>NB</strong>. While developing a “vertical” range of APIs, following the principles stated in the <a href="#back-compat-iceberg-waterline">“On the Waterline of the Iceberg”</a> chapter is crucial. You might manipulate widget content and behavior if, and only if, developers can't “escape the sandbox,” i.e., have direct access to low-level objects encapsulated within the widget.</p>
|
||||
<p><strong>NB</strong>. While developing a “vertical” range of APIs, following the principles stated in the “<a href="#back-compat-iceberg-waterline">On the Waterline of the Iceberg</a>” chapter is crucial. You might manipulate widget content and behavior if, and only if, developers can't “escape the sandbox,” i.e., have direct access to low-level objects encapsulated within the widget.</p>
|
||||
<p>In general, you should aim to have each partner using the API services in a manner that maximizes your profit as an API vendor. Where the partner doesn't try to make some unique experience and needs just a typical solution, you would benefit from making them use widgets, which are under your full control and thus ease the API version fragmentation problem and allow for experimenting in order to reach your KPIs. Where the partner possesses some unique expertise in the subject area and develops a unique service on top of your API, you would benefit from allowing full freedom in customizing the integration, so they might cover specific market niches and enjoy the advantage of offering more flexibility compared to services using competing APIs.</p><div class="page-break"></div><h3><a href="#api-product-kpi" class="anchor" id="api-product-kpi">Chapter 26. The API Key Performance Indicators</a><a href="#chapter-26" class="secondary-anchor" id="chapter-26"> </a></h3>
|
||||
<p>As we described in the previous chapters, there are many API monetization models, both direct and indirect. Importantly, most of them are fully or conditionally free for partners, and the direct-to-indirect benefits ratio tends to change during the API lifecycle. That naturally leads us to the question of how exactly shall we measure the API success and what goals are to be set for the product team.</p>
|
||||
<p>Of course, the most explicit metric is money: if your API is monetized directly or attracts visitors to a monetized service, the rest of the chapter will be of little interest to you, maybe just as a case study. If, however, the contribution of the API to the company's income cannot be simply measured, you have to stick to other, synthetic, indicators.</p>
|
||||
@ -3769,7 +3769,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<p><strong>Importantly</strong>, when we talk about “users,” we will have to make duplicate systems to observe them both using tokens (cookies, logins, phone numbers) and IP addresses, as malefactors aren't obliged to preserve the tokens between requests, or might keep a pool of them to impede their exposure.</p>
|
||||
<h5><a href="#chapter-28-paragraph-2" id="chapter-28-paragraph-2" class="anchor">2. Requesting an Additional Authentication Factor</a></h5>
|
||||
<p>As both static and behavioral analyses are heuristic, it's highly desirable to not make decisions based solely on their outcome but rather ask the suspicious users to additionally prove they're making legitimate requests. If such a mechanism is in place, the quality of an anti-fraud system will be dramatically improved, as it allows for increasing system sensitivity and enabling pro-active defense, e.g., asking users to pass the tests in advance.</p>
|
||||
<p>In the case of services for end users, the main method of acquiring the second factor is redirecting to a captcha page. In the case of APIs it might be problematic, especially if you initially neglected the “Stipulate Restrictions” rule we've given in the <a href="#api-design-describing-interfaces">“Describing Final Interfaces”</a> chapter. In many cases, you will have to impose this responsibility on partners (i.e., it will be partners who show captchas and identify users based on the signals received from the API endpoints). This will, of course, significantly impair the convenience of working with the API.</p>
|
||||
<p>In the case of services for end users, the main method of acquiring the second factor is redirecting to a captcha page. In the case of APIs it might be problematic, especially if you initially neglected the “Stipulate Restrictions” rule we've given in the “<a href="#api-design-describing-interfaces">Describing Final Interfaces</a>” chapter. In many cases, you will have to impose this responsibility on partners (i.e., it will be partners who show captchas and identify users based on the signals received from the API endpoints). This will, of course, significantly impair the convenience of working with the API.</p>
|
||||
<p><strong>NB</strong>. Instead of captcha, there might be other actions introducing additional authentication factors. It might be the phone number confirmation or the second step of the 3D-Secure protocol. The important part is that requesting an additional authentication step must be stipulated in the program interface, as it can't be added later in a backwards-compatible manner.</p>
|
||||
<p>Other popular mechanics of identifying robots include offering a bait (“honeypot”) or employing the execution environment checks (starting from rather trivial ones like executing JavaScript on the webpage and ending with sophisticated techniques of checking application integrity checksums).</p>
|
||||
<h5><a href="#chapter-28-paragraph-3" id="chapter-28-paragraph-3" class="anchor">3. Restricting Access</a></h5>
|
||||
@ -3854,7 +3854,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<p>The inverse scenario: partners must pay for technical support, and it's the API developers who answer the questions. It doesn't actually make a significant difference in terms of the quality of the issues (it's still mostly inexperienced developers who can't solve the problem on their own; you will just cut off those who can't afford paid support) but at least you won't have a hiring problem as you might allow yourself the luxury of having engineers for the first line of support.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Partly (or, sometimes, fully) the developer community might help with solving the amateur problems (see the <a href="#api-product-devrel">“Communicating with Developers”</a> chapter). Usually, community members are pretty capable of answering those questions, especially if moderators help them.</p>
|
||||
<p>Partly (or, sometimes, fully) the developer community might help with solving the amateur problems (see the “<a href="#api-product-devrel">Communicating with Developers</a>” chapter). Usually, community members are pretty capable of answering those questions, especially if moderators help them.</p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>Importantly, whatever options you choose, it's still the API developers in the second line of support simply because only they can fully understand the problem and the partners' code. That implies two important consequences:</p>
|
||||
@ -3974,7 +3974,7 @@ ProgramContext.dispatch = (action) => {
|
||||
<p>Finally, the last aspect we would like to shed the light on is managing partners' expectations regarding the further development of the API. If we talk about consumer qualities, APIs differ little from other B2B software products: in both cases, you need to form some understanding of SLA conditions, available features, interface responsiveness and other characteristics that are important for clients. Still, APIs have their specificities</p>
|
||||
<h4>Versioning and Application Lifecycle</h4>
|
||||
<p>Ideally, the API once published should live eternally; but as we all are reasonable people, we do understand it's impossible in the real life. Even if we continue supporting older versions, they will still become outdated eventually, and partners will need to rewrite the code to use newer functionality.</p>
|
||||
<p>The author of this book formulates the rule of issuing new major API versions like this: the period of time after which partners will need to rewrite the code should coincide with the application lifespan in the subject area (see <a href="#back-compat-statement">“The Backward Compatibility Problem Statement”</a> chapter). Apart from updating <em>major</em> versions, sooner or later you will face issues with accessing some outdated <em>minor</em> versions as well. As we mentioned in the <a href="#back-compat-iceberg-waterline">“On the Waterline of the Iceberg”</a> chapter, even fixing bugs might eventually lead to breaking some integrations, and that naturally leads us to the necessity of keeping older <em>minor</em> versions of the API until the partner resolves the problem.</p>
|
||||
<p>The author of this book formulates the rule of issuing new major API versions like this: the period of time after which partners will need to rewrite the code should coincide with the application lifespan in the subject area (see “<a href="#back-compat-statement">The Backward Compatibility Problem Statement</a>” chapter). Apart from updating <em>major</em> versions, sooner or later you will face issues with accessing some outdated <em>minor</em> versions as well. As we mentioned in the “<a href="#back-compat-iceberg-waterline">On the Waterline of the Iceberg</a>” chapter, even fixing bugs might eventually lead to breaking some integrations, and that naturally leads us to the necessity of keeping older <em>minor</em> versions of the API until the partner resolves the problem.</p>
|
||||
<p>In this aspect, integrating with large companies that have a dedicated software engineering department differs dramatically from providing a solution to individual amateur programmers: on one hand, the former are much more likely to find undocumented features and unfixed bugs in your code; on the other hand, because of the internal bureaucracy, fixing the related issues might easily take months, save not years. The common recommendation there is to maintain old minor API versions for a period of time long enough for the most dilatory partner to switch no the newest version.</p>
|
||||
<h4>Supporting Platforms</h4>
|
||||
<p>Another aspect crucial to interacting with large integrators is supporting a zoo of platforms (browsers, programming languages, protocols, operating systems) and their versions. As usual, big companies have their own policies on which platforms they support, and these policies might sometimes contradict common sense. (Let's say, it's rather a time to abandon TLS 1.2, but many integrators continue working through this protocol, or even the earlier ones.)</p>
|
||||
|
Binary file not shown.
Binary file not shown.
@ -1511,14 +1511,14 @@ app.display(offers);
|
||||
<p>Дополнительно правильная декомпозиция поможет нам в решении задачи расширения и развития API, о чём мы поговорим в разделе II.</p><div class="page-break"></div><h3><a href="#api-design-describing-interfaces" class="anchor" id="api-design-describing-interfaces">Глава 11. Описание конечных интерфейсов</a><a href="#chapter-11" class="secondary-anchor" id="chapter-11"> </a></h3>
|
||||
<p>Определив все сущности, их ответственность и отношения друг с другом, мы переходим непосредственно к разработке API: нам осталось прописать номенклатуру всех объектов, полей, методов и функций в деталях. В этой главе мы дадим сугубо практические советы, как сделать API удобным и понятным.</p>
|
||||
<p>Важное уточнение под номером ноль:</p>
|
||||
<h5><a href="#chapter-11-paragraph-0" id="chapter-11-paragraph-0" class="anchor">0. Правила не должны применяться бездумно</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-1" id="chapter-11-paragraph-1" class="anchor">0. Правила не должны применяться бездумно</a></h5>
|
||||
<p>Правило — это просто кратко сформулированное обобщение опыта. Они не действуют безусловно и не означают, что можно не думать головой. У каждого правила есть какая-то рациональная причина его существования. Если в вашей ситуации нет причин следовать правилу — значит, следовать ему не нужно.</p>
|
||||
<p>Например, требование консистентности номенклатуры существует затем, чтобы разработчик тратил меньше времени на чтение документации; если вам <em>необходимо</em>, чтобы разработчик обязательно прочитал документацию по какому-то методу, вполне разумно сделать его сигнатуру нарочито неконсистентно.</p>
|
||||
<p>Это соображение применимо ко всем принципам ниже. Если из-за следования правилам у вас получается неудобный, громоздкий, неочевидный API — это повод пересмотреть правила (или API).</p>
|
||||
<p>Важно понимать, что вы вольны вводить свои собственные конвенции. Например, в некоторых фреймворках сознательно отказываются от парных методов <code>set_entity</code> / <code>get_entity</code> в пользу одного метода <code>entity</code> с опциональным параметром. Важно только проявить последовательность в её применении — если такая конвенция вводится, то абсолютно все методы API должны иметь подобную полиморфную сигнатуру, или по крайней мере должен существовать принцип именования, отличающий такие комбинированные методы от обычных вызовов.</p>
|
||||
<h4>Обеспечение читабельности и консистентности</h4>
|
||||
<p>Важнейшая задача разработчика API — добиться того, чтобы код, написанный поверх API другими разработчиками, легко читался и поддерживался. Помните, что закон больших чисел работает против вас: если какую-то концепцию или сигнатуру вызова можно понять неправильно, значит, её неизбежно будет понимать неправильно всё большее число партнеров по мере роста популярности API.</p>
|
||||
<h5><a href="#chapter-11-paragraph-1" id="chapter-11-paragraph-1" class="anchor">1. Явное лучше неявного</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-2" id="chapter-11-paragraph-2" class="anchor">2. Явное лучше неявного</a></h5>
|
||||
<p>Из названия любой сущности должно быть очевидно, что она делает, и к каким побочным эффектам может привести её использование.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Отменяет заказ
|
||||
@ -1547,7 +1547,7 @@ orders.calculateAggregatedStats({
|
||||
<p>Два важных следствия:</p>
|
||||
<p><strong>1.1.</strong> Если операция модифицирующая, это должно быть очевидно из сигнатуры. В частности, не может быть модифицирующих операций за <code>GET</code>.</p>
|
||||
<p><strong>1.2.</strong> Если в номенклатуре вашего API есть как синхронные операции, так и асинхронные, то (а)синхронность должна быть очевидна из сигнатур, <strong>либо</strong> должна существовать конвенция именования, позволяющая отличать синхронные операции от асинхронных.</p>
|
||||
<h5><a href="#chapter-11-paragraph-2" id="chapter-11-paragraph-2" class="anchor">2. Указывайте использованные стандарты</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-3" id="chapter-11-paragraph-3" class="anchor">3. Указывайте использованные стандарты</a></h5>
|
||||
<p>К сожалению, человечество не в состоянии договориться о таких простейших вещах, как «с какого дня начинается неделя». Поэтому <em>всегда</em> указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе.</p>
|
||||
<p><strong>Плохо</strong>: <code>"date": "11/12/2020"</code> — существует огромное количество стандартов записи дат, плюс из этой записи невозможно даже понять, что здесь число, а что месяц.</p>
|
||||
<p><strong>Хорошо</strong>: <code>"iso_date": "2020-11-12"</code>.</p>
|
||||
@ -1566,11 +1566,11 @@ orders.calculateAggregatedStats({
|
||||
</code></pre>
|
||||
<p>Отдельное следствие из этого правила — денежные величины <em>всегда</em> должны сопровождаться указанием кода валюты.</p>
|
||||
<p>Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что, как ни сделай, — кто-то останется недовольным. Классический пример такого рода — порядок географических координат («широта-долгота» против «долгота-широта»). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе II.</p>
|
||||
<h5><a href="#chapter-11-paragraph-3" id="chapter-11-paragraph-3" class="anchor">3. Сущности должны именоваться конкретно</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-4" id="chapter-11-paragraph-4" class="anchor">4. Сущности должны именоваться конкретно</a></h5>
|
||||
<p>Избегайте одиночных слов-«амёб» без определённой семантики, таких как get, apply, make.</p>
|
||||
<p><strong>Плохо</strong>: <code>user.get()</code> — неочевидно, что конкретно будет возвращено.</p>
|
||||
<p><strong>Хорошо</strong>: <code>user.get_id()</code>.</p>
|
||||
<h5><a href="#chapter-11-paragraph-4" id="chapter-11-paragraph-4" class="anchor">4. Не экономьте буквы</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-5" id="chapter-11-paragraph-5" class="anchor">5. Не экономьте буквы</a></h5>
|
||||
<p>В XXI веке давно уже нет нужды называть переменные покороче.</p>
|
||||
<p><strong>Плохо</strong>: <code>order.time()</code> — неясно, о каком времени идёт речь: время создания заказа, время готовности заказа, время ожидания заказа?…</p>
|
||||
<p><strong>Хорошо</strong>:</p>
|
||||
@ -1592,7 +1592,7 @@ strpbrk (str1, str2)
|
||||
</code></pre>
|
||||
<p>— однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение <code>string</code> до <code>str</code> выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.</p>
|
||||
<p><strong>NB</strong>: иногда названия полей сокращают или вовсе опускают (например, возвращают массив разнородных объектов вместо набора именованных полей) в погоне за уменьшением количества трафика. В абсолютном большинстве случаев это бессмысленно, поскольку текстовые данные при передаче обычно дополнительно сжимают на уровне протокола.</p>
|
||||
<h5><a href="#chapter-11-paragraph-5" id="chapter-11-paragraph-5" class="anchor">5. Тип поля должен быть ясен из его названия</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-6" id="chapter-11-paragraph-6" class="anchor">6. Тип поля должен быть ясен из его названия</a></h5>
|
||||
<p>Если поле называется <code>recipe</code> — мы ожидаем, что его значением является сущность типа <code>Recipe</code>. Если поле называется <code>recipe_id</code> — мы ожидаем, что его значением является идентификатор, который мы сможем найти в составе сущности <code>Recipe</code>.</p>
|
||||
<p>То же касается и примитивных типов. Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — <code>objects</code>, <code>children</code>; если это невозможно (термин неисчисляем), следует добавить префикс или постфикс, не оставляющий сомнений.</p>
|
||||
<p><strong>Плохо</strong>: <code>GET /news</code> — неясно, будет ли получена какая-то конкретная новость или массив новостей.</p>
|
||||
@ -1612,7 +1612,7 @@ GET /coffee-machines/{id}/functions
|
||||
<pre><code>GET /v1/coffee-machines/{id}⮠
|
||||
/builtin-functions-list
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-11-paragraph-6" id="chapter-11-paragraph-6" class="anchor">6. Подобные сущности должны называться подобно и вести себя подобным образом</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-7" id="chapter-11-paragraph-7" class="anchor">7. Подобные сущности должны называться подобно и вести себя подобным образом</a></h5>
|
||||
<p><strong>Плохо</strong>: <code>begin_transition</code> / <code>stop_transition</code><br>
|
||||
— <code>begin</code> и <code>stop</code> — непарные термины; разработчик будет вынужден рыться в документации.</p>
|
||||
<p><strong>Хорошо</strong>: <code>begin_transition</code> / <code>end_transition</code> либо <code>start_transition</code> / <code>stop_transition</code>.</p>
|
||||
@ -1634,7 +1634,7 @@ str_replace(needle, replace, haystack)
|
||||
<li>первый из методов находит только первое вхождение строки <code>needle</code>, а другой — все вхождения, и об этом поведении никак нельзя узнать из сигнатуры функций.</li>
|
||||
</ul>
|
||||
<p>Упражнение «как сделать эти интерфейсы хорошо» предоставим читателю.</p>
|
||||
<h5><a href="#chapter-11-paragraph-7" id="chapter-11-paragraph-7" class="anchor">7. Избегайте двойных отрицаний</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-8" id="chapter-11-paragraph-8" class="anchor">8. Избегайте двойных отрицаний</a></h5>
|
||||
<p><strong>Плохо</strong>: <code>"dont_call_me": false</code><br>
|
||||
— люди в целом плохо считывают двойные отрицания. Это провоцирует ошибки.</p>
|
||||
<p><strong>Лучше</strong>: <code>"prohibit_calling": true</code> или <code>"avoid_calling": true</code><br>
|
||||
@ -1654,7 +1654,7 @@ str_replace(needle, replace, haystack)
|
||||
}
|
||||
</code></pre>
|
||||
<p>— то разработчику потребуется вычислить флаг <code>!beans_absence && !cup_absence</code>, что эквивалентно <code>!(beans_absence || cup_absence)</code>, а вот в этом переходе ошибиться очень легко, и избегание двойных отрицаний помогает слабо. Здесь, к сожалению, есть только общий совет «избегайте ситуаций, когда разработчику нужно вычислять такие флаги».</p>
|
||||
<h5><a href="#chapter-11-paragraph-8" id="chapter-11-paragraph-8" class="anchor">8. Избегайте неявного приведения типов</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-9" id="chapter-11-paragraph-9" class="anchor">9. Избегайте неявного приведения типов</a></h5>
|
||||
<p>Этот совет парадоксально противоположен предыдущему. Часто при разработке API возникает ситуация, когда добавляется новое необязательное поле с непустым значением по умолчанию. Например:</p>
|
||||
<pre><code>const orderParams = {
|
||||
contactless_delivery: false
|
||||
@ -1720,7 +1720,7 @@ PUT /v1/users/{id}
|
||||
}
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>: противоречие с предыдущим советом состоит в том, что мы специально ввели отрицающий флаг («нет лимита»), который по правилу двойных отрицаний пришлось переименовать в <code>abolish_spending_limit</code>. Хотя это и хорошее название для отрицательного флага, семантика его довольно неочевидна, разработчикам придётся как минимум покопаться в документации. Таков путь.</p>
|
||||
<h5><a href="#chapter-11-paragraph-9" id="chapter-11-paragraph-9" class="anchor">9. Отсутствие результата — тоже результат</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-10" id="chapter-11-paragraph-10" class="anchor">10. Отсутствие результата — тоже результат</a></h5>
|
||||
<p>Если сервер корректно обработал вопрос и никакой внештатной ситуации не возникло — следовательно, это не ошибка. К сожалению, весьма распространён антипаттерн, когда отсутствие результата считается ошибкой.</p>
|
||||
<p><strong>Плохо</strong></p>
|
||||
<pre><code>POST /v1/coffee-machines/search
|
||||
@ -1747,7 +1747,7 @@ PUT /v1/users/{id}
|
||||
}
|
||||
</code></pre>
|
||||
<p>Это правило вообще можно упростить до следующего: если результатом операции является массив данных, то пустота этого массива — не ошибка, а штатный ответ. (Если, конечно, он допустим по смыслу; пустой массив координат, например, является ошибкой.)</p>
|
||||
<h5><a href="#chapter-11-paragraph-10" id="chapter-11-paragraph-10" class="anchor">10. Ошибки должны быть информативными</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-11" id="chapter-11-paragraph-11" class="anchor">11. Ошибки должны быть информативными</a></h5>
|
||||
<p>При написании кода разработчик неизбежно столкнётся с ошибками, в том числе самого примитивного толка: неправильный тип параметра или неверное значение. Чем понятнее ошибки, возвращаемые вашим API, тем меньше времени разработчик потратит на борьбу с ними, и тем приятнее работать с таким API.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>POST /v1/coffee-machines/search
|
||||
@ -1794,7 +1794,7 @@ PUT /v1/users/{id}
|
||||
}
|
||||
</code></pre>
|
||||
<p>Также хорошей практикой является указание всех допущенных ошибок, а не только первой найденной.</p>
|
||||
<h5><a href="#chapter-11-paragraph-11" id="chapter-11-paragraph-11" class="anchor">11. Соблюдайте правильный порядок ошибок</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-12" id="chapter-11-paragraph-12" class="anchor">12. Соблюдайте правильный порядок ошибок</a></h5>
|
||||
<p><strong>Во-первых</strong>, всегда показывайте неразрешимые ошибки прежде разрешимых:</p>
|
||||
<pre><code>POST /v1/orders
|
||||
{
|
||||
@ -1892,7 +1892,7 @@ POST /v1/orders
|
||||
<p>Легко заметить, что в этом примере нет способа разрешить ошибку в один шаг — эту ситуацию требуется предусмотреть отдельно, и либо изменить параметры расчёта (минимальная сумма заказа не учитывает скидки), либо ввести специальную ошибку для такого кейса.</p>
|
||||
<h4>Правила разработки машиночитаемых интерфейсов</h4>
|
||||
<p>В погоне за понятностью API для людей мы часто забываем, что работать с API всё-таки будут не сами разработчики, а написанный ими код. Многие концепции, которые хорошо работают для визуальных интерфейсов, плохо подходят для интерфейсов программных: в частности, разработчик не может в коде принимать решения, ориентируясь на текстовые сообщения, и не может «выйти и зайти снова» в случае нештатной ситуации.</p>
|
||||
<h5><a href="#chapter-11-paragraph-12" id="chapter-11-paragraph-12" class="anchor">12. Состояние системы должно быть понятно клиенту</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-13" id="chapter-11-paragraph-13" class="anchor">13. Состояние системы должно быть понятно клиенту</a></h5>
|
||||
<p>Часто можно встретить интерфейсы, в которых клиент не обладает полнотой знаний о том, что происходит в системе от его имени — например, какие операции сейчас выполняются и каков их статус.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Создаёт заказ и возвращает его id
|
||||
@ -1959,7 +1959,7 @@ GET /v1/users/{id}/orders
|
||||
]
|
||||
}
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-11-paragraph-13" id="chapter-11-paragraph-13" class="anchor">13. Указывайте время жизни ресурсов и политики кэширования</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-14" id="chapter-11-paragraph-14" class="anchor">14. Указывайте время жизни ресурсов и политики кэширования</a></h5>
|
||||
<p>В современных системах клиент, как правило, обладает собственным состоянием и почти всегда кэширует результаты запросов — неважно, долговременно ли или в течение сессии: у каждого объекта всегда есть какое-то время автономной жизни. Поэтому желательно вносить ясность; каким образом рекомендуется кэшировать результат должно быть понятно, если не из сигнатур операций, то хотя бы из документации.</p>
|
||||
<p>Следует уточнить, что кэш мы понимаем в расширенном смысле, а именно: какое варьирование параметров операции (не только времени обращения, но и прочих переменных) следует считать достаточно близким к предыдущему запросу, чтобы можно было использовать результат из кэша?</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
@ -2002,7 +2002,7 @@ GET /v1/price?recipe=lungo⮠
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-11-paragraph-14" id="chapter-11-paragraph-14" class="anchor">14. Пагинация, фильтрация и курсоры</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-15" id="chapter-11-paragraph-15" class="anchor">15. Пагинация, фильтрация и курсоры</a></h5>
|
||||
<p>Любой эндпойнт, возвращающий массивы данных, должен содержать пагинацию. Никаких исключений в этом правиле быть не может.</p>
|
||||
<p>Любой эндпойнт, возвращающий изменяемые данные постранично, должен обеспечивать возможность эти данные перебрать.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
@ -2117,11 +2117,11 @@ GET /v1/records/modified/list⮠
|
||||
<p>Недостатком этой схемы является необходимость заводить отдельное индексированное хранилище событий, а также появление множества событий для одной записи, если данные меняются часто.</p>
|
||||
<h4>Техническое качество API</h4>
|
||||
<p>Хороший API должен не просто решать проблемы разработчиков и пользователей, но и делать это максимально качественно, т.е. не содержать в себе логических и технических ошибок (и не провоцировать на них разработчика), экономить вычислительные ресурсы и вообще имплементировать лучшие практики в своей предметной области.</p>
|
||||
<h5><a href="#chapter-11-paragraph-15" id="chapter-11-paragraph-15" class="anchor">15. Сохраняйте точность дробных чисел</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-16" id="chapter-11-paragraph-16" class="anchor">16. Сохраняйте точность дробных чисел</a></h5>
|
||||
<p>Там, где это позволено протоколом, дробные числа с фиксированной запятой — такие, как денежные суммы, например — должны передаваться в виде специально предназначенных для этого объектов, например, Decimal или аналогичных.</p>
|
||||
<p>Если в протоколе нет Decimal-типов (в частности, в JSON нет чисел с фиксированной запятой), следует либо привести к целому (путём домножения на указанный множитель), либо использовать строковый тип.</p>
|
||||
<p>Если конвертация в формат с плавающей запятой заведомо приводит к потере точности (например, если мы переведём 20 минут в часы в виде десятичной дроби), то следует либо предпочесть формат без потери точности (т.е. предпочесть формат <code>00:20</code> формату <code>0.333333…</code>), либо предоставить SDK работы с такими данными, либо (в крайнем случае) описать в документации принципы округления.</p>
|
||||
<h5><a href="#chapter-11-paragraph-16" id="chapter-11-paragraph-16" class="anchor">16. Все операции должны быть идемпотентны</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-17" id="chapter-11-paragraph-17" class="anchor">17. Все операции должны быть идемпотентны</a></h5>
|
||||
<p>Напомним, идемпотентность — это следующее свойство: повторный вызов той же операции с теми же параметрами не изменяет результат. Поскольку мы обсуждаем в первую очередь клиент-серверное взаимодействие, узким местом в котором является ненадежность сетевой составляющей, повтор запроса при обрыве соединения — не исключительная ситуация, а норма жизни.</p>
|
||||
<p>Там, где идемпотентность не может быть обеспечена естественным образом, необходимо добавить явный параметр — ключ идемпотентности или ревизию.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
@ -2188,7 +2188,7 @@ X-Idempotency-Token: <токен>
|
||||
<li>нельзя полагаться на то, что клиенты генерируют честные случайные токены — они могут иметь одинаковый seed рандомизатора или просто использовать слабый алгоритм или источник энтропии; при проверке токенов нужны слабые ограничения: уникальность токена должна проверяться не глобально, а только применительно к конкретному пользователю и конкретной операции;</li>
|
||||
<li>клиенты склонны неправильно понимать концепцию — или генерировать новый токен на каждый перезапрос (что на самом деле неопасно, в худшем случае деградирует UX), или, напротив, использовать один токен для разнородных запросов (а вот это опасно и может привести к катастрофически последствиям; ещё одна причина имплементировать совет из предыдущего пункта!); поэтому рекомендуется написать хорошую документацию и/или клиентскую библиотеку для перезапросов.</li>
|
||||
</ul>
|
||||
<h5><a href="#chapter-11-paragraph-17" id="chapter-11-paragraph-17" class="anchor">17. Избегайте неатомарных операций</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-18" id="chapter-11-paragraph-18" class="anchor">18. Избегайте неатомарных операций</a></h5>
|
||||
<p>С применением массива изменений часто возникает вопрос: что делать, если часть изменений удалось применить, а часть — нет? Здесь правило очень простое: если вы можете обеспечить атомарность, т.е. выполнить либо все изменения сразу, либо ни одно из них — сделайте это.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Возвращает список рецептов
|
||||
@ -2325,15 +2325,15 @@ api.getRecipes();
|
||||
<p>По сути, для клиента всё произошло ожидаемым образом: изменения были внесены, и последний полученный ответ всегда корректен. Однако по сути состояние ресурса после первого запроса отличалось от состояния ресурса после второго запроса, что противоречит самому определению идемпотентности.</p>
|
||||
<p>Более корректно было бы при получении повторного запроса с тем же токеном ничего не делать и возвращать ту же разбивку ошибок, что была дана на первый запрос — но для этого придётся её каким-то образом хранить в истории изменений.</p>
|
||||
<p>На всякий случай уточним, что вложенные операции должны быть сами по себе идемпотентны. Если же это не так, то следует сгенерировать внутренние ключи идемпотентности на каждую вложенную операцию в отдельности.</p>
|
||||
<h5><a href="#chapter-11-paragraph-18" id="chapter-11-paragraph-18" class="anchor">18. Не изобретайте безопасность</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-19" id="chapter-11-paragraph-19" class="anchor">19. Не изобретайте безопасность</a></h5>
|
||||
<p>Если бы автору этой книги давали доллар каждый раз, когда ему приходилось бы имплементировать кем-то придуманный дополнительный протокол безопасности — он бы давно уже был на заслуженной пенсии. Любовь разработчиков API к подписыванию параметры запросов или сложным схемам обмена паролей на токены столь же несомненна, сколько и бессмысленна.</p>
|
||||
<p><strong>Во-первых</strong>, почти всегда процедуры, обеспечивающие безопасность той или иной операции, <em>уже разработаны</em>. Нет никакой нужды придумывать их заново, просто имплементируйте какой-то из существующих протоколов. Никакие самописные алгоритмы проверки сигнатур запросов не обеспечат вам того же уровня защиты от атаки <a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">Man-in-the-Middle</a>, как соединение по протоколу TLS с взаимной проверкой сигнатур сертификатов.</p>
|
||||
<p><strong>Во-вторых</strong>, чрезвычайно самонадеянно (и опасно) считать, что вы разбираетесь в вопросах безопасности. Новые вектора атаки появляются каждый день, и быть в курсе всех актуальных проблем — это само по себе работа на полный рабочий день. Если же вы полный рабочий день занимаетесь чем-то другим, спроектированная вами система защиты наверняка будет содержать уязвимости, о которых вы просто никогда не слышали — например, ваш алгоритм проверки паролей может быть подвержен <a href="https://en.wikipedia.org/wiki/Timing_attack">атаке по времени</a>, а веб-сервер — <a href="https://capec.mitre.org/data/definitions/105.html">атаке с разделением запросов</a>.</p>
|
||||
<h5><a href="#chapter-11-paragraph-19" id="chapter-11-paragraph-19" class="anchor">19. Декларируйте технические ограничения явно</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-20" id="chapter-11-paragraph-20" class="anchor">20. Декларируйте технические ограничения явно</a></h5>
|
||||
<p>У любого поля в вашем API есть ограничения на допустимые значения: максимальная длина текста, объём прикладываемых документов в мегабайтах, разрешённые диапазоны цифровых значений. Часто разработчики API пренебрегают указанием этих лимитов — либо потому, что считают их очевидными, либо потому, что попросту не знают их сами. Это, разумеется, один большой антипаттерн: незнание пределов использования системы автоматически означает, что код партнёров может в любой момент перестать работать по не зависящим от них причинам.</p>
|
||||
<p>Поэтому, во-первых, указывайте границы допустимых значений для всех без исключения полей в API, и, во-вторых, если эти границы нарушены, генерируйте машиночитаемую ошибку с описанием, какое ограничение на какое поле было нарушено.</p>
|
||||
<p>То же соображение применимо и к квотам: партнёры должны иметь доступ к информации о том, какую долю доступных ресурсов они выбрали, и ошибки в случае превышения квоты должны быть информативными.</p>
|
||||
<h5><a href="#chapter-11-paragraph-20" id="chapter-11-paragraph-20" class="anchor">20. Считайте трафик</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-21" id="chapter-11-paragraph-21" class="anchor">21. Считайте трафик</a></h5>
|
||||
<p>В современном мире такой ресурс, как объём пропущенного трафика, считать уже почти не принято — считается, что Интернет всюду практически безлимитен. Однако он всё-таки не абсолютно безлимитен: всегда можно спроектировать систему так, что объём трафика окажется некомфортным даже и для современных сетей.</p>
|
||||
<p>Три основные причины раздувания объёма трафика достаточно очевидны:</p>
|
||||
<ul>
|
||||
@ -2364,7 +2364,7 @@ api.getRecipes();
|
||||
</li>
|
||||
</ul>
|
||||
<p>Неплохим упражнением здесь будет промоделировать типовой жизненный цикл основной функциональности приложения партнёра (например, выполнение одного заказа) и подсчитать общее количество запросов и объём трафика на один цикл.</p>
|
||||
<h5><a href="#chapter-11-paragraph-21" id="chapter-11-paragraph-21" class="anchor">21. Избегайте неявных частичных обновлений</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-22" id="chapter-11-paragraph-22" class="anchor">22. Избегайте неявных частичных обновлений</a></h5>
|
||||
<p>Один из самых частых антипаттернов в разработке API — попытка сэкономить на подробном описании изменения состояния.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Создаёт заказ из двух напитков
|
||||
@ -2486,19 +2486,19 @@ X-Idempotency-Token: <токен идемпотентности>
|
||||
<p>Этот подход существенно сложнее в имплементации, но является единственным возможным вариантом реализации совместного редактирования, поскольку он явно отражает, что в действительности делал пользовать с представлением объекта. Имея данные в таком формате возможно организовать и оффлайн-редактирование, когда пользовательские изменения накапливаются и сервер впоследствии автоматически разрешает конфликты, «перебазируя» изменения.</p>
|
||||
<h4>Продуктовое качество API</h4>
|
||||
<p>Помимо технологических ограничений, любой реальный API скоро столкнётся и с несовершенством окружающей действительности. Конечно, мы все хотели бы жить в мире розовых единорогов, свободном от накопления legacy, злоумышленников, национальных конфликтов и происков конкурентов. Но, к сожалению или к счастью, живём мы в реальном мире, в котором хороший API должен учитывать всё вышеперечисленное.</p>
|
||||
<h5><a href="#chapter-11-paragraph-22" id="chapter-11-paragraph-22" class="anchor">22. Используйте глобально уникальные идентификаторы</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-23" id="chapter-11-paragraph-23" class="anchor">23. Используйте глобально уникальные идентификаторы</a></h5>
|
||||
<p>Хорошим тоном при разработке API будет использование для идентификаторов сущностей глобально уникальных строк, либо семантичных (например, "lungo" для видов напитков), либо случайных (например <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)">UUID-4</a>). Это может чрезвычайно пригодиться, если вдруг придётся объединять данные из нескольких источников под одним идентификатором.</p>
|
||||
<p>Мы вообще склонны порекомендовать использование идентификаторов в urn-подобном формате, т.е. <code>urn:order:<uuid></code> (или просто <code>order:<uuid></code>), это сильно помогает с отладкой legacy-систем, где по историческим причинам есть несколько разных идентификаторов для одной и той же сущности, в таком случае неймспейсы в urn помогут быстро понять, что это за идентификатор и нет ли здесь ошибки использования.</p>
|
||||
<p>Отдельное важное следствие: <strong>не используйте инкрементальные номера как идентификаторы</strong>. Помимо вышесказанного, это плохо ещё и тем, что ваши конкуренты легко смогут подсчитать, сколько у вас в системе каких сущностей и тем самым вычислить, например, точное количество заказов за каждый день наблюдений.</p>
|
||||
<p><strong>NB</strong>: в этой книге часто используются короткие идентификаторы типа "123" в примерах — это для удобства чтения на маленьких экранах, повторять эту практику в реальном API не надо.</p>
|
||||
<h5><a href="#chapter-11-paragraph-23" id="chapter-11-paragraph-23" class="anchor">23. Предусмотрите ограничения доступа</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-24" id="chapter-11-paragraph-24" class="anchor">24. Предусмотрите ограничения доступа</a></h5>
|
||||
<p>С ростом популярности API вам неизбежно придётся внедрять технические средства защиты от недобросовестного использования — такие, как показ капчи, расстановка приманок-honeypot-ов, возврат ошибок вида «слишком много запросов», постановка прокси-защиты от DDoS перед эндпойнтами и так далее. Всё это невозможно сделать, если вы не предусмотрели такой возможности изначально, а именно — не ввели соответствующей номенклатуры ошибок и предупреждений.</p>
|
||||
<p>Вы не обязаны с самого начала такие ошибки действительно генерировать — но вы можете предусмотреть их на будущее. Например, вы можете описать ошибку <code>429 Too Many Requests</code> или перенаправление на показ капчи, но не имплементировать возврат таких ответов, пока не возникнет в этом необходимость.</p>
|
||||
<p>Отдельно необходимо уточнить, что в тех случаях, когда через API можно совершать платежи, ввод дополнительных факторов аутентификации пользователя (через TOTP, SMS или технологии типа 3D-Secure) должен быть предусмотрен обязательно.</p>
|
||||
<h5><a href="#chapter-11-paragraph-24" id="chapter-11-paragraph-24" class="anchor">24. Не предоставляйте endpoint-ов массового получения чувствительных данных</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-25" id="chapter-11-paragraph-25" class="anchor">25. Не предоставляйте endpoint-ов массового получения чувствительных данных</a></h5>
|
||||
<p>Если через API возможно получение персональных данных, номер банковских карт, переписки пользователей и прочей информации, раскрытие которой нанесёт большой ущерб пользователям, партнёрам и/или вам — методов массового получения таких данных в API быть не должно, или, по крайней мере, на них должны быть ограничения на частоту запросов, размер страницы данных, а в идеале ещё и многофакторная аутентификация.</p>
|
||||
<p>Часто разумной практикой является предоставление таких массовых выгрузок по запросу, т.е. фактически в обход API.</p>
|
||||
<h5><a href="#chapter-11-paragraph-25" id="chapter-11-paragraph-25" class="anchor">25. Локализация и интернационализация</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-26" id="chapter-11-paragraph-26" class="anchor">26. Локализация и интернационализация</a></h5>
|
||||
<p>Все эндпойнты должны принимать на вход языковые параметры (например, в виде заголовка <code>Accept-Language</code>), даже если на текущем этапе нужды в локализации нет.</p>
|
||||
<p>Важно понимать, что язык пользователя и юрисдикция, в которой пользователь находится — разные вещи. Цикл работы вашего API всегда должен хранить локацию пользователя. Либо она задаётся явно (в запросе указываются географические координаты), либо неявно (первый запрос с географическими координатами инициировал создание сессии, в которой сохранена локация) — но без локации корректная локализация невозможна. В большинстве случаев локацию допустимо редуцировать до кода страны.</p>
|
||||
<p>Дело в том, что множество параметров, потенциально влияющих на работу API, зависят не от языка, а именно от расположения пользователя. В частности, правила форматирования чисел (разделители целой и дробной частей, разделители разрядов) и дат, первый день недели, раскладка клавиатуры, система единиц измерения (которая к тому же может оказаться не десятичной!) и так далее. В некоторых ситуациях необходимо хранить две локации: та, в которой пользователь находится, и та, которую пользователь сейчас просматривает. Например, если пользователь из США планирует туристическую поездку в Европу, то цены ему желательно показывать в местной валюте, но отформатированными согласно правилам американского письма.</p>
|
||||
|
Binary file not shown.
@ -6,7 +6,7 @@
|
||||
"repository": "github.com:twirl/The-API-Book",
|
||||
"version": "2.0.0",
|
||||
"devDependencies": {
|
||||
"@twirl/book-builder": "0.0.22",
|
||||
"@twirl/book-builder": "0.0.23",
|
||||
"html-docx-js": "^0.3.1",
|
||||
"nodemon": "^2.0.19",
|
||||
"puppeteer": "^13.1.2"
|
||||
|
@ -11,4 +11,4 @@ The sentences “a major API version” and “a new API version, containing bac
|
||||
|
||||
It is usually (though not necessary) agreed that the last stable API release might be referenced by either a full version (e.g., `1.2.3`) or a reduced one (`1.2` or just `1`). Some systems support more sophisticated schemes for defining the desired version (for example, `^1.2.3` reads like “get the last stable API release that is backward-compatible to the `1.2.3` version”) or additional shortcuts (for example, `1.2-beta` to refer to the last beta release of the `1.2` API version family). In this book, we will mostly use designations like `v1` (`v2`, `v3`, etc.) to denote the latest stable release of the `1.x.x` version family of an API.
|
||||
|
||||
The practical meaning of this versioning system and the applicable policies will be discussed in more detail in the [“Backward Compatibility Problem Statement”](#back-compat-statement) chapter.
|
||||
The practical meaning of this versioning system and the applicable policies will be discussed in more detail in the “[Backward Compatibility Problem Statement](#back-compat-statement)” chapter.
|
||||
|
@ -330,7 +330,7 @@ There are three obvious reasons for inflating network traffic:
|
||||
|
||||
All these problems must be solved with setting limitations on field sizes and properly decomposing endpoints. If some entity comprises both “lightweight” data (let's say, the name and the description of the recipe) and “heavy” data (let's say, the promo picture of the 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 (a link to the image, in our case) — this will allow at least 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 (for example, making a single order) to count the number of requests and the amount of traffic that it takes. It might turn out that the reason for the increased amount of requests / network traffic consumption was a mistake made in the design of state change notification endpoints. We will discuss this issue in detail in the [“Bidirectional Data Flow”](#api-patterns-push-vs-poll) chapter of “The API Patterns” section of this book.
|
||||
As a useful exercise, try modeling the typical lifecycle of a partner's app's main functionality (for example, making a single order) to count the number of requests and the amount of traffic that it takes. It might turn out that the reason for the increased amount of requests / network traffic consumption was a mistake made in the design of state change notification endpoints. We will discuss this issue in detail in the “[Bidirectional Data Flow](#api-patterns-push-vs-poll)” chapter of “The API Patterns” section of this book.
|
||||
|
||||
##### No Results Is a Result
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
### [On Design Patterns in the API Context][api-patterns-context]
|
||||
|
||||
The concept of [“Patterns”](https://en.wikipedia.org/wiki/Software_design_pattern#History) in the field of software engineering was introduced by Kent Beck and Ward Cunningham in 1987 and popularized by “The Gang of Four” (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in their book “Design Patterns: Elements of Reusable Object-Oriented Software,” which was published in 1994. According to the most widespread definition, a software design pattern is a “general, reusable solution to a commonly occurring problem within a given context.”
|
||||
The concept of “[Patterns](https://en.wikipedia.org/wiki/Software_design_pattern#History)” in the field of software engineering was introduced by Kent Beck and Ward Cunningham in 1987 and popularized by “The Gang of Four” (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in their book “Design Patterns: Elements of Reusable Object-Oriented Software,” which was published in 1994. According to the most widespread definition, a software design pattern is a “general, reusable solution to a commonly occurring problem within a given context.”
|
||||
|
||||
If we talk about APIs, especially those to which developers are end users (e.g., frameworks or operating system interfaces), the classical software design patterns are well applicable to them. Indeed, many examples in the previous Section of this book are just about applying some design patterns.
|
||||
|
||||
|
@ -22,7 +22,7 @@ try {
|
||||
|
||||
As orders are created much more rarely than read, we might significantly increase the system performance if we drop the requirement of returning the most recent state of the resource from the state retrieval endpoints. The versioning will help us avoid possible problems: creating an order will still be impossible unless the client has the actual version. In fact, we transited to the [eventual consistency](https://en.wikipedia.org/wiki/Consistency_model#Eventual_consistency) model: the client will be able to fulfill its request *sometime* when it finally gets the actual data. In modern microservice architectures, eventual consistency is rather an industrial standard, and it might be close to impossible to achieve the opposite, i.e., strict consistency.
|
||||
|
||||
**NB**: let us stress that you might choose the approach only in the case of exposing new APIs. If you're already providing an endpoint implementing some consistency model, you can't just lower the consistency level (for instance, introduce eventual consistency instead of the strict one) even if you never documented the behavior. This will be discussed in detail in the [“On the Waterline of the Iceberg”](#back-compat-iceberg-waterline) chapter of “The Backward Compatibility” section of this book.
|
||||
**NB**: let us stress that you might choose the approach only in the case of exposing new APIs. If you're already providing an endpoint implementing some consistency model, you can't just lower the consistency level (for instance, introduce eventual consistency instead of the strict one) even if you never documented the behavior. This will be discussed in detail in the “[On the Waterline of the Iceberg](#back-compat-iceberg-waterline)” chapter of “The Backward Compatibility” section of this book.
|
||||
|
||||
Choosing weak consistency instead of a strict one, however, brings some disadvantages. For instance, we might require partners to wait until they get the actual resource state to make changes — but it is quite unobvious for partners (and actually inconvenient) they must be prepared to wait for changes they made themselves to propagate.
|
||||
|
||||
@ -38,7 +38,7 @@ const pendingOrders = await api.
|
||||
|
||||
If strict consistency is not guaranteed, the second call might easily return an empty result as it reads data from a replica, and the newest order might not have hit it yet.
|
||||
|
||||
An important pattern that helps in this situation is implementing the [“read-your-writes”](https://en.wikipedia.org/wiki/Consistency_model#Read-your-writes_consistency) model, i.e., guaranteeing that clients observe the changes they have just made. The consistency might be lifted to the read-your-writes level by making clients pass some token that describes the last changes known to the client.
|
||||
An important pattern that helps in this situation is implementing the “[read-your-writes](https://en.wikipedia.org/wiki/Consistency_model#Read-your-writes_consistency)” model, i.e., guaranteeing that clients observe the changes they have just made. The consistency might be lifted to the read-your-writes level by making clients pass some token that describes the last changes known to the client.
|
||||
|
||||
```
|
||||
const order = await api
|
||||
@ -74,7 +74,7 @@ There is also an important question regarding the default behavior of the server
|
||||
|
||||
Let us state an important assertion: the methods of solving architectural problems we're discussing in this section *are probabilistic*. Abolishing strict consistency means that, even if all components of the system work perfectly, client errors will still occur — and we may only try to lessen their numbers for typical usage profiles.
|
||||
|
||||
**NB**: the “typical usage profile” stipulation is important: an API implies the variability of client scenarios, and API usage cases might fall into several groups, each featuring quite different error profiles. The classical example is client APIs (where it's an end user who makes actions and waits for results) versus server APIs (where the execution time is per se not so important — but let's say mass parallel execution might be). If this happens, it's a strong signal to make a family of API products covering different usage scenarios, as we will discuss in [“The API Services Range”](#api-product-range) chapter of “The API Product” section of this book.
|
||||
**NB**: the “typical usage profile” stipulation is important: an API implies the variability of client scenarios, and API usage cases might fall into several groups, each featuring quite different error profiles. The classical example is client APIs (where it's an end user who makes actions and waits for results) versus server APIs (where the execution time is per se not so important — but let's say mass parallel execution might be). If this happens, it's a strong signal to make a family of API products covering different usage scenarios, as we will discuss in “[The API Services Range](#api-product-range)” chapter of “The API Product” section of this book.
|
||||
|
||||
Let's return to the coffee example, and imagine we implemented the following scheme:
|
||||
* optimistic concurrency control (through, let's say, the id of the last user's order)
|
||||
|
@ -55,8 +55,8 @@ Thus we naturally came to the pattern of organizing asynchronous APIs through ta
|
||||
|
||||
The asynchronous call pattern is useful for solving other practical tasks as well:
|
||||
* caching operation results and providing links to them (implying that if the client needs to reread the operation result or share it with another client, it might use the task identifier to do so)
|
||||
* ensuring operation idempotency (through introducing the task confirmation step we will actually get the draft-commit system as discussed in the [“Describing Final Interfaces”](#api-design-describing-interfaces) chapter)
|
||||
* naturally improving resilience to peak loads on the service as the new tasks will be queuing up (possibly prioritized) in fact implementing the [“token bucket”](https://en.wikipedia.org/wiki/Token_bucket) technique
|
||||
* ensuring operation idempotency (through introducing the task confirmation step we will actually get the draft-commit system as discussed in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter)
|
||||
* naturally improving resilience to peak loads on the service as the new tasks will be queuing up (possibly prioritized) in fact implementing the “[token bucket](https://en.wikipedia.org/wiki/Token_bucket)” technique
|
||||
* organizing interaction in the cases of very long-lasting operations that require more time than typical timeouts (which are tens of seconds in the case of network calls) or can take unpredictable time.
|
||||
|
||||
Also, asynchronous communication is more robust from a future API development point of view: request handling procedures might evolve towards prolonging and extending the asynchronous execution pipelines whereas synchronous handlers must retain reasonable execution times which puts certain restrictions on possible internal architecture.
|
||||
|
@ -12,7 +12,7 @@ const pendingOrders = await api
|
||||
}, …]}
|
||||
```
|
||||
|
||||
However, an attentive reader might notice that this interface violates the recommendation we previously gave in the [“Describing Final Interfaces”](#api-design-describing-interfaces) chapter: the returned data volume must be limited, but there are no restrictions in our design. This problem was already present in the previous versions of the endpoint, but abolishing asynchronous order creation makes it much worse. The task creation operation must work as quickly as possible, and therefore, almost all limit checks are to be executed asynchronously. As a result, a client might easily create a large number of ongoing tasks which would potentially inflate the size of the `getOngoingOrders` response.
|
||||
However, an attentive reader might notice that this interface violates the recommendation we previously gave in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter: the returned data volume must be limited, but there are no restrictions in our design. This problem was already present in the previous versions of the endpoint, but abolishing asynchronous order creation makes it much worse. The task creation operation must work as quickly as possible, and therefore, almost all limit checks are to be executed asynchronously. As a result, a client might easily create a large number of ongoing tasks which would potentially inflate the size of the `getOngoingOrders` response.
|
||||
|
||||
**NB**: having *no limit at all* on order task creation is unwise, and there must be some (involving as lightweight checks as possible). Let us, however, focus on the response size issue in this chapter.
|
||||
|
||||
@ -223,7 +223,7 @@ GET /v1/partners/{id}/offers/history⮠
|
||||
|
||||
The first request format allows for implementing the first scenario, i.e., retrieving the fresh portion of the data. Conversely, the second format makes it possible to consistently iterate over the data to fulfill the second scenario. Importantly, the second request is cacheable as the tail of the list never changes.
|
||||
|
||||
**NB**: in the [“Describing Final Interfaces”](#api-design-describing-interfaces) chapter we recommended avoiding exposing incremental identifiers in publicly accessible APIs. Note that the scheme described above might be augmented to comply with this rule by exposing some arbitrary secondary identifiers. The requirement is that these identifiers might be unequivocally converted into monotonous ones.
|
||||
**NB**: in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter we recommended avoiding exposing incremental identifiers in publicly accessible APIs. Note that the scheme described above might be augmented to comply with this rule by exposing some arbitrary secondary identifiers. The requirement is that these identifiers might be unequivocally converted into monotonous ones.
|
||||
|
||||
Another possible anchor to rely on is the record creation date. However, this approach is harder to implement for the following reasons:
|
||||
* Creation dates for two records might be identical, especially if the records are mass-generated programmatically. In the worst-case scenario, it might happen that at some specific moment, more records were created than one request page contains making it impossible to traverse them.
|
||||
|
@ -78,7 +78,7 @@ To integrate via a *webhook*, a partner specifies a URL of their own message pro
|
||||
|
||||
Let us imagine that in our coffee example the partner has a backend capable of processing newly created orders to be processed by partner's coffee shops, and we need to organize such communication. Realizing this task comprise several steps:
|
||||
|
||||
##### Negotiate a Contract
|
||||
##### 1. Negotiate a Contract
|
||||
|
||||
Depending on how important the partner is for our business, different options are possible:
|
||||
* The API vendor might develop the functionality of calling the partner's *webhook* utilizing a protocol proposed by the partner
|
||||
@ -87,11 +87,11 @@ Depending on how important the partner is for our business, different options ar
|
||||
|
||||
What is important is that the *must* be a formal contract (preferably in a form of a specification) for *webhook*'s request and response formats and all the errors that might happen.
|
||||
|
||||
##### Agree on Authorization and Authentication Methods
|
||||
##### 2. Agree on Authorization and Authentication Methods
|
||||
|
||||
As a *webhook* is a callback channel, you will need to develop a separate authorization system to deal with it as it's *partners* duty to check that the request is genuinely coming from the API backend, not vice versa. We reiterate here our strictest recommendation to stick to existing standard techniques, for example, [mTLS](https://en.wikipedia.org/wiki/Mutual_authentication#mTLS); though in the real world, you will likely have to use archaic methods like fixing the caller server's IP address.
|
||||
|
||||
##### Develop an Interface for Setting the URL of a *Webhook*
|
||||
##### 3. Develop an Interface for Setting the URL of a *Webhook*
|
||||
|
||||
As the callback endpoint is developed by partners, we do not know its URL beforehand. It implies some interface must exist for setting this URL and authorized public keys (probably in a form of a control panel for partners).
|
||||
|
||||
@ -129,7 +129,7 @@ Obviously, we can't guarantee partners don't make any of these mistakes. The onl
|
||||
|
||||
As for internal APIs, the *webhook* technology (i.e., the possibility to programmatically define a callback URL) is either not needed at all or is replaced with the [Service Discovery](https://en.wikipedia.org/wiki/Web_Services_Discovery) protocol as services comprising a single backend are symmetrically able to call each other. However, the problems of callback-based integration discussed above are equally actual for internal calls. Requesting an internal API might result in a false-negative mistake, internal clients might be unaware that ordering is not guaranteed, etc.
|
||||
|
||||
To solve these problems, and also to ensure better horizontal scalability, [message queues](https://en.wikipedia.org/wiki/Message_queue) were developed, most notably numerous pub/sub pattern implementations. At present moment, pub/sub-based architectures are very popular in enterprise software development, up to switch any inter-service communication to message queues.
|
||||
To solve these problems, and also to ensure better horizontal scalability, [message queues](https://en.wikipedia.org/wiki/Message_queue) were developed, most notably numerous pub/sub pattern implementations. At present moment, pub/sub-based architectures are very popular in enterprise software development, up to switching any inter-service communication to message queues.
|
||||
|
||||
**NB**: let us note that everything comes with a price, and these delivery guarantees and horizontal scalability are not an exclusion:
|
||||
* all communication becomes eventually consistent with all the implications
|
||||
|
@ -122,4 +122,4 @@ The important (and undeniable) advantage of the *semver* system is that it provi
|
||||
|
||||
Of course, preserving minor versions infinitely isn't possible (partly because of security and compliance issues that tend to pile up). However, providing such access for a reasonable period of time is rather a hygienic norm for popular APIs.
|
||||
|
||||
**NB**. Sometimes to defend the single accessible API version concept, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backward compatibility as it might be relying on some un-versioned services (for example, some data in the DB that are shared between all the API versions). We, however, consider this an additional reason to isolate such dependencies (see [“The Serenity Notepad”](#back-compat-serenity-notepad) chapter) as it means that changes to these subsystems might lead to the inoperability of the API.
|
||||
**NB**. Sometimes to defend the single accessible API version concept, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backward compatibility as it might be relying on some un-versioned services (for example, some data in the DB that are shared between all the API versions). We, however, consider this an additional reason to isolate such dependencies (see “[The Serenity Notepad](#back-compat-serenity-notepad)” chapter) as it means that changes to these subsystems might lead to the inoperability of the API.
|
@ -22,7 +22,7 @@ POST /v1/recipes
|
||||
|
||||
At first glance, again, it looks like a reasonably simple interface, explicitly decomposed into abstraction levels. But let us imagine the future — what would happen with this interface when our system evolves further?
|
||||
|
||||
The first problem is obvious to those who read the [“Describing Final Interfaces”](#api-design-describing-interfaces) chapter thoroughly: product properties must be localized. That will lead us to the first change:
|
||||
The first problem is obvious to those who read the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter thoroughly: product properties must be localized. That will lead us to the first change:
|
||||
|
||||
```
|
||||
"product_properties": {
|
||||
@ -190,7 +190,7 @@ POST /v1/recipe-builder
|
||||
}
|
||||
```
|
||||
|
||||
We should also note that providing a newly created entity identifier by the requesting side isn't exactly the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, we're risking getting lots of collisions on recipe names used by different partners, so we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the [“Describing Final Interfaces”](#api-design-describing-interfaces) chapter.
|
||||
We should also note that providing a newly created entity identifier by the requesting side isn't exactly the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, we're risking getting lots of collisions on recipe names used by different partners, so we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter.
|
||||
|
||||
```
|
||||
POST /v1/recipes/custom
|
||||
@ -211,7 +211,7 @@ POST /v1/recipes/custom
|
||||
|
||||
Also note that this format allows us to maintain an important extensibility point: different partners might have both shared and isolated namespaces. Furthermore, we might introduce special namespaces (like `common`, for example) to allow editing standard recipes (and thus organizing our own recipes backoffice).
|
||||
|
||||
**NB**: a mindful reader might have noted that this technique was already used in our API study much earlier in the [“Separating Abstraction Levels”](#api-design-separating-abstractions) chapter with regards to the “program” and “program run” entities. Indeed, we might do it without the `program-matcher` endpoint and make it this way:
|
||||
**NB**: a mindful reader might have noted that this technique was already used in our API study much earlier in the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter with regards to the “program” and “program run” entities. Indeed, we might do it without the `program-matcher` endpoint and make it this way:
|
||||
|
||||
```
|
||||
GET /v1/recipes/{id}/run-data/{api_type}
|
||||
|
@ -1,6 +1,6 @@
|
||||
### [Weak Coupling][back-compat-weak-coupling]
|
||||
|
||||
In the previous chapter, we've demonstrated how breaking strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. But let us return to the question we have previously mentioned in the [“Extending through Abstracting”](#back-compat-abstracting-extending) chapter: how should we parametrize the order preparation process implemented via a third-party API? In other words, what *is* the `order_execution_endpoint` required in the API type registration handler?
|
||||
In the previous chapter, we've demonstrated how breaking strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. But let us return to the question we have previously mentioned in the “[Extending through Abstracting](#back-compat-abstracting-extending)” chapter: how should we parametrize the order preparation process implemented via a third-party API? In other words, what *is* the `order_execution_endpoint` required in the API type registration handler?
|
||||
|
||||
```
|
||||
PUT /v1/api-types/{api_type}
|
||||
@ -59,7 +59,7 @@ So, how would we tackle this issue? Using one of two possible approaches: either
|
||||
* the higher-level program API level doesn't actually know how the execution of its commands works; it formulates the tasks at its own level of understanding: brew this recipe, send user's requests to a partner, allow the user to collect their order;
|
||||
* the underlying program execution API level doesn't care what other same-level implementations exist; it just interprets those parts of the task that make sense to it.
|
||||
|
||||
If we take a look at the principles described in the previous chapter, we would find that this principle was already formulated: we need to describe *informational contexts* at every abstraction level and design a mechanism to translate them between levels. Furthermore, in a more general sense, we formulated it as early as in “The Data Flow” paragraph of the [“Separating Abstraction Levels”](#api-design-separating-abstractions) chapter.
|
||||
If we take a look at the principles described in the previous chapter, we would find that this principle was already formulated: we need to describe *informational contexts* at every abstraction level and design a mechanism to translate them between levels. Furthermore, in a more general sense, we formulated it as early as in “The Data Flow” paragraph of the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter.
|
||||
|
||||
In our case we need to implement the following mechanisms:
|
||||
* running a program creates a corresponding context comprising all the essential parameters;
|
||||
@ -223,4 +223,4 @@ From what was said, one more important conclusion follows: doing a real job, i.e
|
||||
|
||||
Contrariwise, applying the paradigm of concretizing the contexts at each new abstraction level, we will eventually fall into the bunny hole deep enough to have nothing to concretize: the context itself unambiguously matches the functionality we can programmatically control. And at that level, we must stop detailing contexts further, and just realize the algorithms needed. It's worth mentioning that the abstraction deepness for different underlying platforms might vary.
|
||||
|
||||
**NB**. In the [“Separating Abstraction Levels”](#api-design-separating-abstractions) chapter we have illustrated exactly this: when we speak about the first coffee machine API type, there is no need to extend the tree of abstractions further than running programs, but with the second API type, we need one more intermediary abstraction level, namely the runtimes API.
|
||||
**NB**. In the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter we have illustrated exactly this: when we speak about the first coffee machine API type, there is no need to extend the tree of abstractions further than running programs, but with the second API type, we need one more intermediary abstraction level, namely the runtimes API.
|
@ -23,7 +23,7 @@ Then we would have come to the understanding that a “search result” is actua
|
||||
|
||||
So our interface (let us call it `ISearchResult`) is actually a composition of two other interfaces: `IOrderParameters` (an entity that allows for creating an order) and `ISearchItemViewParameters` (some abstract representation of the search result in the UI). This interface split should automatically lead us to additional questions:
|
||||
|
||||
1. How will we couple the former and the latter? Obviously, these two sub-interfaces are related: the machine-readable price must match the human-readable one, for example. This will naturally lead us to the “formatter” concept described in the [“Strong Coupling and Related Problems”](#back-compat-strong-coupling) chapter.
|
||||
1. How will we couple the former and the latter? Obviously, these two sub-interfaces are related: the machine-readable price must match the human-readable one, for example. This will naturally lead us to the “formatter” concept described in the “[Strong Coupling and Related Problems](#back-compat-strong-coupling)” chapter.
|
||||
|
||||
2. And what is the “abstract representation of the search result in the UI”? Do we have other kinds of search, should the `ISearchItemViewParameters` interface be a subtype of some even more general interface, or maybe a composition of several such ones?
|
||||
|
||||
|
@ -20,7 +20,7 @@ Any software must be tested, and APIs ain't an exclusion. However, there are som
|
||||
|
||||
##### Isolate the Dependencies
|
||||
|
||||
In the case of a gateway API that provides access to some underlying API or aggregates several APIs behind a single façade, there is a strong temptation to proxy the original interface as is, thus not introducing any changes to it and making life much simpler by sparing an effort needed to implement the weak-coupled interaction between services. For example, while developing program execution interfaces as described in the [“Separating Abstraction Levels”](#api-design-separating-abstractions) chapter we might have taken the existing first-kind coffee-machine API as a role model and provided it in our API by just proxying the requests and responses as is. Doing so is highly undesirable because of several reasons:
|
||||
In the case of a gateway API that provides access to some underlying API or aggregates several APIs behind a single façade, there is a strong temptation to proxy the original interface as is, thus not introducing any changes to it and making life much simpler by sparing an effort needed to implement the weak-coupled interaction between services. For example, while developing program execution interfaces as described in the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter we might have taken the existing first-kind coffee-machine API as a role model and provided it in our API by just proxying the requests and responses as is. Doing so is highly undesirable because of several reasons:
|
||||
* usually, you have no guarantees that the partner will maintain backward compatibility or at least keep new versions more or less conceptually akin to the older ones;
|
||||
* any partner's problem will automatically ricochet into your customers.
|
||||
|
||||
@ -36,7 +36,7 @@ The best practice is quite the opposite: isolate the third-party API usage, i.e.
|
||||
|
||||
There is an antipattern that occurs frequently: API developers use some internal closed implementations of some methods which exist in the public API. It happens because of two reasons:
|
||||
* often the public API is just an addition to the existing specialized software, and the functionality, exposed via the API, isn't being ported back to the closed part of the project, or the public API developers simply don't know the corresponding internal functionality exists;
|
||||
* in the course of extending the API, some interfaces become abstract, but the existing functionality isn't affected; imagine that while implementing the `PUT /formatters` interface described in the [“Strong Coupling and Related Problems”](#back-compat-strong-coupling) chapter API developers have created a new, more general version of the volume formatter but hasn't changed the implementation of the existing one, so it continues working for pre-existing languages.
|
||||
* in the course of extending the API, some interfaces become abstract, but the existing functionality isn't affected; imagine that while implementing the `PUT /formatters` interface described in the “[Strong Coupling and Related Problems](#back-compat-strong-coupling)” chapter API developers have created a new, more general version of the volume formatter but hasn't changed the implementation of the existing one, so it continues working for pre-existing languages.
|
||||
|
||||
There are obvious local problems with this approach (like the inconsistency in functions' behavior, or the bugs which were not found while testing the code), but also a bigger one: your API might be simply unusable if a developer tries any non-mainstream approach, because of performance issues, bugs, instability, etc., as the API developers themselves never tried to use this public interface for anything important.
|
||||
|
||||
|
@ -45,7 +45,7 @@ B2B services are a special case. As B2B Service providers benefit from offering
|
||||
* internal customers employ quite a specific technological stack, and the API is poorly optimized to work with other programming languages / operating systems / frameworks;
|
||||
* for external customers, the learning curve will be pretty flat as they can't take a look at the source code or talk to the API developers directly, unlike internal customers that are much more familiar with the API concepts;
|
||||
* documentation often covers only some subset of use cases needed by internal customers;
|
||||
* the API services ecosystem which we will describe in [“The API Services Range”](#api-product-range) chapter usually doesn't exist.
|
||||
* the API services ecosystem which we will describe in “[The API Services Range](#api-product-range)” chapter usually doesn't exist.
|
||||
* Any resources spent are directed to covering internal customer needs first. It means the following:
|
||||
* API development plans are totally opaque to partners, and sometimes look just absurd with obvious problems being neglected for years;
|
||||
* technical support of external customers is financed on leftovers.
|
||||
|
@ -17,7 +17,7 @@ As both approaches are still heuristic, the API product vision is inevitably fuz
|
||||
|
||||
The same fuzziness should be kept in mind while making interviews and getting feedback. Software engineers will mainly report the problems they've got with the technical integrations, and rarely speak of business-related issues; meanwhile, business owners care little about the inconvenience of writing code. Both will have some knowledge regarding the end users' problems, but it's usually limited to the market segment the partner operates on.
|
||||
|
||||
If you do have an access to end users' actions monitoring (see [“The API Key Performance Indicators”](#api-product-kpi) chapter), then you might try to analyze the typical user behavior through these logs and understand how users interact with the partners' applications. But you will need to make this analysis on a per-application basis and try to clusterize the most common scenarios.
|
||||
If you do have an access to end users' actions monitoring (see “[The API Key Performance Indicators](#api-product-kpi)” chapter), then you might try to analyze the typical user behavior through these logs and understand how users interact with the partners' applications. But you will need to make this analysis on a per-application basis and try to clusterize the most common scenarios.
|
||||
|
||||
#### Checking Product Hypotheses
|
||||
|
||||
|
@ -57,4 +57,4 @@ This fact greatly affects everything we had discussed previously (except for, ma
|
||||
* A huge share of customers' inquiries to your customer support service will be generated by the first category of developers: it's much harder for amateurs or beginners to find answers to their questions by themselves, and they will address them to you.
|
||||
* At the same time, the second category is much more sensitive to the quality of both the product and customer support, and fulfilling their requests might be non-trivial.
|
||||
|
||||
Finally, it's almost impossible in a course of a single product to create an API that will fit well both amateur and professional developers: the former need the maximum simplicity of implementing basic use cases, while the latter seek the ability to adapt the API to match technological stack and development paradigms, and the problems they solve usually require deep customization. We will discuss the matter in [“The API Services Range”](#api-product-range) chapter.
|
||||
Finally, it's almost impossible in a course of a single product to create an API that will fit well both amateur and professional developers: the former need the maximum simplicity of implementing basic use cases, while the latter seek the ability to adapt the API to match technological stack and development paradigms, and the problems they solve usually require deep customization. We will discuss the matter in “[The API Services Range](#api-product-range)” chapter.
|
||||
|
@ -17,7 +17,7 @@ Different companies employ different approaches to determining the granularity o
|
||||
#### Vertical Scaling of API Services
|
||||
|
||||
However, frequently it makes sense to provide several API services manipulating the same functionality. Let us remind you that there are two kinds of developers: professional ones that seek extensive customization capabilities (as they usually work in big IT companies that have a specific mindset towards integrations), and semi-professionals who just need the gentlest possible learning curve. The only way to cover the needs of both categories is to develop a range of products with different entry thresholds and requirements for developers' professional level. We might name several API sub-types, ordered from the most technically demanding to less complex ones.
|
||||
1. The most advanced level is that of physical APIs and the abstractions on top of them. [In our coffee example, the collection of entities describing working with APIs of physical coffee machines, see the [“Separating Abstraction Levels”](#api-design-separating-abstractions) and the [“Weak Coupling”](#back-compat-weak-coupling) chapters.]
|
||||
1. The most advanced level is that of physical APIs and the abstractions on top of them. [In our coffee example, the collection of entities describing working with APIs of physical coffee machines, see the “[Separating Abstraction Levels](#api-design-separating-abstractions)” and the “[Weak Coupling](#back-compat-weak-coupling)” chapters.]
|
||||
2. The basic level of working with product entities via formal interfaces. [In our study example, that will be HTTP API for making orders.]
|
||||
3. Working with product entities might be simplified if SDKs are provided for some popular platforms that tailor API concepts according to the paradigms of those platforms (for those developers who are proficient with specific platforms only that will save a lot of effort on dealing with formal protocols and interfaces).
|
||||
4. The next simplification step is providing services for code generation. In this service, developers choose one of the pre-built integration templates, customize some options, and got a ready-to-use piece of code that might be simply copy-pasted into the application code (and might be additionally customized by adding some level 1-3 code). This approach is sometimes called “point-and-click programming.” [In the case of our coffee API, an example of such a service might have a form or screen editor for a developer to place UI elements and get the working application code.]
|
||||
@ -32,6 +32,6 @@ The important advantage of having a range of APIs is not only about adapting it
|
||||
4. Code generation makes it possible to manipulate the desired form of integrations. For example, if our KPI is a number of searches performed through the API, we might alter the generated code so it will show the search panel in the most convenient position in the app; as partners using code-generation services rarely make any changes in the resulting code, and this will help us in reaching the goal.
|
||||
5. Finally, ready-to-use components and widgets are under your full control, and you might experiment with functionality exposed through them in partners' applications just as if it was your own service. (However, it doesn't automatically mean that you might draw some profits from having this control; for example, if you're allowing inserting pictures by their direct URL, your control over this integration is rather negligible, so it's generally better to provide those kinds of integration that allow having more control over the functionality in partners' apps.)
|
||||
|
||||
**NB**. While developing a “vertical” range of APIs, following the principles stated in the [“On the Waterline of the Iceberg”](#back-compat-iceberg-waterline) chapter is crucial. You might manipulate widget content and behavior if, and only if, developers can't “escape the sandbox,” i.e., have direct access to low-level objects encapsulated within the widget.
|
||||
**NB**. While developing a “vertical” range of APIs, following the principles stated in the “[On the Waterline of the Iceberg](#back-compat-iceberg-waterline)” chapter is crucial. You might manipulate widget content and behavior if, and only if, developers can't “escape the sandbox,” i.e., have direct access to low-level objects encapsulated within the widget.
|
||||
|
||||
In general, you should aim to have each partner using the API services in a manner that maximizes your profit as an API vendor. Where the partner doesn't try to make some unique experience and needs just a typical solution, you would benefit from making them use widgets, which are under your full control and thus ease the API version fragmentation problem and allow for experimenting in order to reach your KPIs. Where the partner possesses some unique expertise in the subject area and develops a unique service on top of your API, you would benefit from allowing full freedom in customizing the integration, so they might cover specific market niches and enjoy the advantage of offering more flexibility compared to services using competing APIs.
|
@ -21,7 +21,7 @@ Generally speaking, there are two approaches we might take, the static one and t
|
||||
|
||||
As both static and behavioral analyses are heuristic, it's highly desirable to not make decisions based solely on their outcome but rather ask the suspicious users to additionally prove they're making legitimate requests. If such a mechanism is in place, the quality of an anti-fraud system will be dramatically improved, as it allows for increasing system sensitivity and enabling pro-active defense, i.e., asking users to pass the tests in advance.
|
||||
|
||||
In the case of services for end users, the main method of acquiring the second factor is redirecting to a captcha page. In the case of APIs it might be problematic, especially if you initially neglected the “Stipulate Restrictions” rule we've given in the [“Describing Final Interfaces”](#api-design-describing-interfaces) chapter. In many cases, you will have to impose this responsibility on partners (i.e., it will be partners who show captchas and identify users based on the signals received from the API endpoints). This will, of course, significantly impair the convenience of working with the API.
|
||||
In the case of services for end users, the main method of acquiring the second factor is redirecting to a captcha page. In the case of APIs it might be problematic, especially if you initially neglected the “Stipulate Restrictions” rule we've given in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter. In many cases, you will have to impose this responsibility on partners (i.e., it will be partners who show captchas and identify users based on the signals received from the API endpoints). This will, of course, significantly impair the convenience of working with the API.
|
||||
|
||||
**NB**. Instead of captcha, there might be other actions introducing additional authentication factors. It might be the phone number confirmation or the second step of the 3D-Secure protocol. The important part is that requesting an additional authentication step must be stipulated in the program interface, as it can't be added later in a backwards-compatible manner.
|
||||
|
||||
|
@ -24,7 +24,7 @@ There are several options for tackling these issues:
|
||||
|
||||
2. The inverse scenario: partners must pay for technical support, and it's the API developers who answer the questions. It doesn't actually make a significant difference in terms of the quality of the issues (it's still mostly inexperienced developers who can't solve the problem on their own; you will just cut off those who can't afford paid support) but at least you won't have a hiring problem as you might allow yourself the luxury of having engineers for the first line of support.
|
||||
|
||||
3. Partly (or, sometimes, fully) the developer community might help with solving the amateur problems (see the [“Communicating with Developers”](#api-product-devrel) chapter). Usually, community members are pretty capable of answering those questions, especially if moderators help them.
|
||||
3. Partly (or, sometimes, fully) the developer community might help with solving the amateur problems (see the “[Communicating with Developers](#api-product-devrel)” chapter). Usually, community members are pretty capable of answering those questions, especially if moderators help them.
|
||||
|
||||
Importantly, whatever options you choose, it's still the API developers in the second line of support simply because only they can fully understand the problem and the partners' code. That implies two important consequences:
|
||||
|
||||
|
@ -6,7 +6,7 @@ Finally, the last aspect we would like to shed the light on is managing partners
|
||||
|
||||
Ideally, the API once published should live eternally; but as we all are reasonable people, we do understand it's impossible in the real life. Even if we continue supporting older versions, they will still become outdated eventually, and partners will need to rewrite the code to use newer functionality.
|
||||
|
||||
The author of this book formulates the rule of issuing new major API versions like this: the period of time after which partners will need to rewrite the code should coincide with the application lifespan in the subject area (see [“The Backward Compatibility Problem Statement”](#back-compat-statement) chapter). Apart from updating *major* versions, sooner or later you will face issues with accessing some outdated *minor* versions as well. As we mentioned in the [“On the Waterline of the Iceberg”](#back-compat-iceberg-waterline) chapter, even fixing bugs might eventually lead to breaking some integrations, and that naturally leads us to the necessity of keeping older *minor* versions of the API until the partner resolves the problem.
|
||||
The author of this book formulates the rule of issuing new major API versions like this: the period of time after which partners will need to rewrite the code should coincide with the application lifespan in the subject area (see “[The Backward Compatibility Problem Statement](#back-compat-statement)” chapter). Apart from updating *major* versions, sooner or later you will face issues with accessing some outdated *minor* versions as well. As we mentioned in the “[On the Waterline of the Iceberg](#back-compat-iceberg-waterline)” chapter, even fixing bugs might eventually lead to breaking some integrations, and that naturally leads us to the necessity of keeping older *minor* versions of the API until the partner resolves the problem.
|
||||
|
||||
In this aspect, integrating with large companies that have a dedicated software engineering department differs dramatically from providing a solution to individual amateur programmers: on one hand, the former are much more likely to find undocumented features and unfixed bugs in your code; on the other hand, because of the internal bureaucracy, fixing the related issues might easily take months, save not years. The common recommendation there is to maintain old minor API versions for a period of time long enough for the most dilatory partner to switch no the newest version.
|
||||
|
||||
|
@ -4,6 +4,8 @@
|
||||
"chapter": "Chapter",
|
||||
"toc": "Table of Contents",
|
||||
"description": "API-first development is one of the hottest technical topics nowadays since many companies started to realize that API serves as a multiplicator to their opportunities—but it also amplifies the design mistakes as well. This book is written to share the expertise and describe the best practices in designing and developing APIs. It comprises six sections dedicated to: the API design, API patterns, maintaining backward compatibility, HTTP API & REST, SDK and UI libraries, API product management.",
|
||||
"publisher": "Sergey Konstantinov",
|
||||
"copyright": "© Sergey Konstantinov, 2023",
|
||||
"locale": "en_US",
|
||||
"file": "API",
|
||||
"aboutMe": {
|
||||
|
@ -77,7 +77,7 @@ GET /v1/orders/created-history⮠
|
||||
|
||||
Предположим, что в нашем кофейном примере партнёр располагает некоторым бэкендом, готовым принимать оповещения о новых заказах, поступивших в его кофейни, и нам нужно договориться о формате взаимодействия. Решение этой задачи декомпозируется на несколько шагов:
|
||||
|
||||
##### Договоренность о контракте
|
||||
##### 1. Договоренность о контракте
|
||||
|
||||
В зависимости от важности партнёра для вашего бизнеса здесь возможны разные варианты:
|
||||
* производитель API может реализовать возможность вызова webhook-а в формате, предложенном партнёром;
|
||||
@ -86,11 +86,11 @@ GET /v1/orders/created-history⮠
|
||||
|
||||
Важно, что в любом случае должен существовать формальный контракт (очень желательно — в виде спецификации) на форматы запросов и ответов эндпойнта-webhook-а и возникающие ошибки.
|
||||
|
||||
##### Договорённость о авторизации и аутентификации
|
||||
##### 2. Договорённость о способах авторизации и аутентификации
|
||||
|
||||
Так как webhook-и представляют собой обратный канал взаимодействия, для него придётся разработать отдельный способ авторизации — это партнёр должен проверить, что запрос исходит от нашего бэкенда, а не наоборот. Мы повторяем здесь настоятельную рекомендацию не изобретать безопасность и использовать существующие стандартные механизмы, например, [mTLS](https://en.wikipedia.org/wiki/Mutual_authentication#mTLS), хотя в реальном мире с большой долей вероятности придётся использовать архаичные техники типа фиксации IP-адреса вызывающего сервера.
|
||||
|
||||
##### API для задания адреса webhook-а
|
||||
##### 3. API для задания адреса webhook-а
|
||||
|
||||
Так как callback-эндпойнт разрабатывается партнёром, его URL нам априори неизвестен. Должен существовать интерфейс (возможно, в виде кабинета партнёра) для задания URL webhook-а (и публичных ключей авторизации).
|
||||
|
||||
|
@ -4,6 +4,8 @@
|
||||
"chapter": "Глава",
|
||||
"toc": "Содержание",
|
||||
"description": "Разработка API — особый навык: API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок. Эта книга написана для того, чтобы поделиться опытом и изложить лучшие практики разработки API. Книга состоит из шести разделов, посвящённых проектированию API, паттернам дизайна API, поддержанию обратной совместимости, HTTP API и REST, SDK и UI-библиотекам, продуктовому управлению API.",
|
||||
"publisher": "Сергей Константинов",
|
||||
"copyright": "© Сергей Константинов, 2023",
|
||||
"locale": "ru_RU",
|
||||
"file": "API",
|
||||
"landingFile": "index.ru.html",
|
||||
|
@ -11,4 +11,4 @@ Sentences “a major API version” and “new API version, containing backwards
|
||||
|
||||
It is usually (though not necessary) agreed that the last stable API release might be referenced by either a full version (e.g., `1.2.3`) or a reduced one (`1.2` or just `1`). Some systems support more sophisticated schemes of defining the desired version (for example, `^1.2.3` reads like “get the last stable API release that is backwards-compatible to the `1.2.3` version”) or additional shortcuts (for example, `1.2-beta` to refer to the last beta release of the `1.2` API version family). In this book, we will mostly use designations like `v1` (`v2`, `v3`, etc.) to denote the latest stable release of the `1.x.x` version family of an API.
|
||||
|
||||
The practical meaning of this versioning system and the applicable policies will be discussed in more detail in [“The Backward Compatibility Problem Statement”](#back-compat-statement) chapter.
|
||||
The practical meaning of this versioning system and the applicable policies will be discussed in more detail in “[The Backward Compatibility Problem Statement](#back-compat-statement)” chapter.
|
||||
|
@ -112,4 +112,4 @@ The important (and undeniable) advantage of the *semver* system is that it provi
|
||||
|
||||
Of course, preserving minor versions infinitely isn't possible (partly because of security and compliance issues that tend to pile up). However, providing such access for a reasonable period of time is rather a hygienic norm for popular APIs.
|
||||
|
||||
**NB**. Sometimes to defend the single accessible API version concept, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backward compatibility as it might be relying on some un-versioned services (for example, some data in the DB that are shared between all the API versions). We, however, consider this an additional reason to isolate such dependencies (see [“The Serenity Notepad”](#back-compat-serenity-notepad) chapter) as it means that changes to these subsystems might lead to the inoperability of the API.
|
||||
**NB**. Sometimes to defend the single accessible API version concept, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backward compatibility as it might be relying on some un-versioned services (for example, some data in the DB that are shared between all the API versions). We, however, consider this an additional reason to isolate such dependencies (see “[The Serenity Notepad](#back-compat-serenity-notepad)” chapter) as it means that changes to these subsystems might lead to the inoperability of the API.
|
@ -22,7 +22,7 @@ POST /v1/recipes
|
||||
|
||||
At first glance, again, it looks like a reasonably simple interface, explicitly decomposed into abstraction levels. But let us imagine the future — what would happen with this interface when our system evolves further?
|
||||
|
||||
The first problem is obvious to those who read the [“Describing Final Interfaces”](#api-design-describing-interfaces) chapter thoroughly: product properties must be localized. That will lead us to the first change:
|
||||
The first problem is obvious to those who read the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter thoroughly: product properties must be localized. That will lead us to the first change:
|
||||
|
||||
```
|
||||
"product_properties": {
|
||||
@ -190,7 +190,7 @@ POST /v1/recipe-builder
|
||||
}
|
||||
```
|
||||
|
||||
We should also note that providing a newly created entity identifier by the requesting side isn't exactly the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, we're risking getting lots of collisions on recipe names used by different partners, so we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the [“Describing Final Interfaces”](#api-design-describing-interfaces) chapter.
|
||||
We should also note that providing a newly created entity identifier by the requesting side isn't exactly the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, we're risking getting lots of collisions on recipe names used by different partners, so we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter.
|
||||
|
||||
```
|
||||
POST /v1/recipes/custom
|
||||
@ -211,7 +211,7 @@ POST /v1/recipes/custom
|
||||
|
||||
Also note that this format allows us to maintain an important extensibility point: different partners might have both shared and isolated namespaces. Furthermore, we might introduce special namespaces (like `common`, for example) to allow editing standard recipes (and thus organizing our own recipes backoffice).
|
||||
|
||||
**NB**: a mindful reader might have noted that this technique was already used in our API study much earlier in the [“Separating Abstraction Levels”](#api-design-separating-abstractions) chapter with regards to the “program” and “program run” entities. Indeed, we might do it without the `program-matcher` endpoint and make it this way:
|
||||
**NB**: a mindful reader might have noted that this technique was already used in our API study much earlier in the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter with regards to the “program” and “program run” entities. Indeed, we might do it without the `program-matcher` endpoint and make it this way:
|
||||
|
||||
```
|
||||
GET /v1/recipes/{id}/run-data/{api_type}
|
||||
|
@ -1,6 +1,6 @@
|
||||
### [Weak Coupling][back-compat-weak-coupling]
|
||||
|
||||
In the previous chapter, we've demonstrated how breaking strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. But let us return to the question we have previously mentioned in the [“Extending through Abstracting”](#back-compat-abstracting-extending) chapter: how should we parametrize the order preparation process implemented via a third-party API? In other words, what *is* the `order_execution_endpoint` required in the API type registration handler?
|
||||
In the previous chapter, we've demonstrated how breaking strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. But let us return to the question we have previously mentioned in the “[Extending through Abstracting](#back-compat-abstracting-extending)” chapter: how should we parametrize the order preparation process implemented via a third-party API? In other words, what *is* the `order_execution_endpoint` required in the API type registration handler?
|
||||
|
||||
```
|
||||
PUT /v1/api-types/{api_type}
|
||||
@ -59,7 +59,7 @@ So, how would we tackle this issue? Using one of two possible approaches: either
|
||||
* the higher-level program API level doesn't actually know how the execution of its commands works; it formulates the tasks at its own level of understanding: brew this recipe, send user's requests to a partner, allow the user to collect their order;
|
||||
* the underlying program execution API level doesn't care what other same-level implementations exist; it just interprets those parts of the task that make sense to it.
|
||||
|
||||
If we take a look at the principles described in the previous chapter, we would find that this principle was already formulated: we need to describe *informational contexts* at every abstraction level and design a mechanism to translate them between levels. Furthermore, in a more general sense, we formulated it as early as in “The Data Flow” paragraph of the [“Separating Abstraction Levels”](#api-design-separating-abstractions) chapter.
|
||||
If we take a look at the principles described in the previous chapter, we would find that this principle was already formulated: we need to describe *informational contexts* at every abstraction level and design a mechanism to translate them between levels. Furthermore, in a more general sense, we formulated it as early as in “The Data Flow” paragraph of the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter.
|
||||
|
||||
In our case we need to implement the following mechanisms:
|
||||
* running a program creates a corresponding context comprising all the essential parameters;
|
||||
@ -223,4 +223,4 @@ From what was said, one more important conclusion follows: doing a real job, i.e
|
||||
|
||||
Contrariwise, applying the paradigm of concretizing the contexts at each new abstraction level, we will eventually fall into the bunny hole deep enough to have nothing to concretize: the context itself unambiguously matches the functionality we can programmatically control. And at that level, we must stop detailing contexts further, and just realize the algorithms needed. It's worth mentioning that the abstraction deepness for different underlying platforms might vary.
|
||||
|
||||
**NB**. In the [“Separating Abstraction Levels”](#api-design-separating-abstractions) chapter we have illustrated exactly this: when we speak about the first coffee machine API type, there is no need to extend the tree of abstractions further than running programs, but with the second API type, we need one more intermediary abstraction level, namely the runtimes API.
|
||||
**NB**. In the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter we have illustrated exactly this: when we speak about the first coffee machine API type, there is no need to extend the tree of abstractions further than running programs, but with the second API type, we need one more intermediary abstraction level, namely the runtimes API.
|
@ -23,7 +23,7 @@ Then we would have come to the understanding that a “search result” is actua
|
||||
|
||||
So our interface (let us call it `ISearchResult`) is actually a composition of two other interfaces: `IOrderParameters` (an entity that allows for creating an order) and `ISearchItemViewParameters` (some abstract representation of the search result in the UI). This interface split should automatically lead us to additional questions:
|
||||
|
||||
1. How will we couple the former and the latter? Obviously, these two sub-interfaces are related: the machine-readable price must match the human-readable one, for example. This will naturally lead us to the “formatter” concept described in the [“Strong Coupling and Related Problems”](#back-compat-strong-coupling) chapter.
|
||||
1. How will we couple the former and the latter? Obviously, these two sub-interfaces are related: the machine-readable price must match the human-readable one, for example. This will naturally lead us to the “formatter” concept described in the “[Strong Coupling and Related Problems](#back-compat-strong-coupling)” chapter.
|
||||
|
||||
2. And what is the “abstract representation of the search result in the UI”? Do we have other kinds of search, should the `ISearchItemViewParameters` interface be a subtype of some even more general interface, or maybe a composition of several such ones?
|
||||
|
||||
|
@ -20,7 +20,7 @@ Any software must be tested, and APIs ain't an exclusion. However, there are som
|
||||
|
||||
##### Isolate the Dependencies
|
||||
|
||||
In the case of a gateway API that provides access to some underlying API or aggregates several APIs behind a single façade, there is a strong temptation to proxy the original interface as is, thus not introducing any changes to it and making life much simpler by sparing an effort needed to implement the weak-coupled interaction between services. For example, while developing program execution interfaces as described in the [“Separating Abstraction Levels”](#api-design-separating-abstractions) chapter we might have taken the existing first-kind coffee-machine API as a role model and provided it in our API by just proxying the requests and responses as is. Doing so is highly undesirable because of several reasons:
|
||||
In the case of a gateway API that provides access to some underlying API or aggregates several APIs behind a single façade, there is a strong temptation to proxy the original interface as is, thus not introducing any changes to it and making life much simpler by sparing an effort needed to implement the weak-coupled interaction between services. For example, while developing program execution interfaces as described in the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter we might have taken the existing first-kind coffee-machine API as a role model and provided it in our API by just proxying the requests and responses as is. Doing so is highly undesirable because of several reasons:
|
||||
* usually, you have no guarantees that the partner will maintain backward compatibility or at least keep new versions more or less conceptually akin to the older ones;
|
||||
* any partner's problem will automatically ricochet into your customers.
|
||||
|
||||
@ -36,7 +36,7 @@ The best practice is quite the opposite: isolate the third-party API usage, i.e.
|
||||
|
||||
There is an antipattern that occurs frequently: API developers use some internal closed implementations of some methods which exist in the public API. It happens because of two reasons:
|
||||
* often the public API is just an addition to the existing specialized software, and the functionality, exposed via the API, isn't being ported back to the closed part of the project, or the public API developers simply don't know the corresponding internal functionality exists;
|
||||
* in the course of extending the API, some interfaces become abstract, but the existing functionality isn't affected; imagine that while implementing the `PUT /formatters` interface described in the [“Strong Coupling and Related Problems”](#back-compat-strong-coupling) chapter API developers have created a new, more general version of the volume formatter but hasn't changed the implementation of the existing one, so it continues working for pre-existing languages.
|
||||
* in the course of extending the API, some interfaces become abstract, but the existing functionality isn't affected; imagine that while implementing the `PUT /formatters` interface described in the “[Strong Coupling and Related Problems](#back-compat-strong-coupling)” chapter API developers have created a new, more general version of the volume formatter but hasn't changed the implementation of the existing one, so it continues working for pre-existing languages.
|
||||
|
||||
There are obvious local problems with this approach (like the inconsistency in functions' behavior, or the bugs which were not found while testing the code), but also a bigger one: your API might be simply unusable if a developer tries any non-mainstream approach, because of performance issues, bugs, instability, etc., as the API developers themselves never tried to use this public interface for anything important.
|
||||
|
||||
|
@ -45,7 +45,7 @@ B2B services are a special case. As B2B Service providers benefit from offering
|
||||
* internal customers employ quite a specific technological stack, and the API is poorly optimized to work with other programming languages / operating systems / frameworks;
|
||||
* for external customers, the learning curve will be pretty flat as they can't take a look at the source code or talk to the API developers directly, unlike internal customers that are much more familiar with the API concepts;
|
||||
* documentation often covers only some subset of use cases needed by internal customers;
|
||||
* the API services ecosystem which we will describe in [“The API Services Range”](#api-product-range) chapter usually doesn't exist.
|
||||
* the API services ecosystem which we will describe in “[The API Services Range](#api-product-range)” chapter usually doesn't exist.
|
||||
* Any resources spent are directed to covering internal customer needs first. It means the following:
|
||||
* API development plans are totally opaque to partners, and sometimes look just absurd with obvious problems being neglected for years;
|
||||
* technical support of external customers is financed on leftovers.
|
||||
|
@ -17,7 +17,7 @@ As both approaches are still heuristic, the API product vision is inevitably fuz
|
||||
|
||||
The same fuzziness should be kept in mind while making interviews and getting feedback. Software engineers will mainly report the problems they've got with the technical integrations, and rarely speak of business-related issues; meanwhile, business owners care little about the inconvenience of writing code. Both will have some knowledge regarding the end users' problems, but it's usually limited to the market segment the partner operates on.
|
||||
|
||||
If you do have an access to end users' actions monitoring (see [“The API Key Performance Indicators”](#api-product-kpi) chapter), then you might try to analyze the typical user behavior through these logs and understand how users interact with the partners' applications. But you will need to make this analysis on a per-application basis and try to clusterize the most common scenarios.
|
||||
If you do have an access to end users' actions monitoring (see “[The API Key Performance Indicators](#api-product-kpi)” chapter), then you might try to analyze the typical user behavior through these logs and understand how users interact with the partners' applications. But you will need to make this analysis on a per-application basis and try to clusterize the most common scenarios.
|
||||
|
||||
#### Checking Product Hypotheses
|
||||
|
||||
|
@ -57,4 +57,4 @@ This fact greatly affects everything we had discussed previously (except for, ma
|
||||
* A huge share of customers' inquiries to your customer support service will be generated by the first category of developers: it's much harder for amateurs or beginners to find answers to their questions by themselves, and they will address them to you.
|
||||
* At the same time, the second category is much more sensitive to the quality of both the product and customer support, and fulfilling their requests might be non-trivial.
|
||||
|
||||
Finally, it's almost impossible in a course of a single product to create an API that will fit well both amateur and professional developers: the former need the maximum simplicity of implementing basic use cases, while the latter seek the ability to adapt the API to match technological stack and development paradigms, and the problems they solve usually require deep customization. We will discuss the matter in [“The API Services Range”](#api-product-range) chapter.
|
||||
Finally, it's almost impossible in a course of a single product to create an API that will fit well both amateur and professional developers: the former need the maximum simplicity of implementing basic use cases, while the latter seek the ability to adapt the API to match technological stack and development paradigms, and the problems they solve usually require deep customization. We will discuss the matter in “[The API Services Range](#api-product-range)” chapter.
|
||||
|
@ -17,7 +17,7 @@ Different companies employ different approaches to determining the granularity o
|
||||
#### Vertical Scaling of API Services
|
||||
|
||||
However, frequently it makes sense to provide several API services manipulating the same functionality. Let us remind you that there are two kinds of developers: professional ones that seek extensive customization capabilities (as they usually work in big IT companies that have a specific mindset towards integrations), and semi-professionals who just need the gentlest possible learning curve. The only way to cover the needs of both categories is to develop a range of products with different entry thresholds and requirements for developers' professional level. We might name several API sub-types, ordered from the most technically demanding to less complex ones.
|
||||
1. The most advanced level is that of physical APIs and the abstractions on top of them. [In our coffee example, the collection of entities describing working with APIs of physical coffee machines, see the [“Separating Abstraction Levels”](#api-design-separating-abstractions) and the [“Weak Coupling”](#back-compat-weak-coupling) chapters.]
|
||||
1. The most advanced level is that of physical APIs and the abstractions on top of them. [In our coffee example, the collection of entities describing working with APIs of physical coffee machines, see the “[Separating Abstraction Levels](#api-design-separating-abstractions)” and the “[Weak Coupling](#back-compat-weak-coupling)” chapters.]
|
||||
2. The basic level of working with product entities via formal interfaces. [In our study example, that will be HTTP API for making orders.]
|
||||
3. Working with product entities might be simplified if SDKs are provided for some popular platforms that tailor API concepts according to the paradigms of those platforms (for those developers who are proficient with specific platforms only that will save a lot of effort on dealing with formal protocols and interfaces).
|
||||
4. The next simplification step is providing services for code generation. In this service, developers choose one of the pre-built integration templates, customize some options, and got a ready-to-use piece of code that might be simply copy-pasted into the application code (and might be additionally customized by adding some level 1-3 code). This approach is sometimes called “point-and-click programming.” [In the case of our coffee API, an example of such a service might have a form or screen editor for a developer to place UI elements and get the working application code.]
|
||||
@ -32,6 +32,6 @@ The important advantage of having a range of APIs is not only about adapting it
|
||||
4. Code generation makes it possible to manipulate the desired form of integrations. For example, if our KPI is a number of searches performed through the API, we might alter the generated code so it will show the search panel in the most convenient position in the app; as partners using code-generation services rarely make any changes in the resulting code, and this will help us in reaching the goal.
|
||||
5. Finally, ready-to-use components and widgets are under your full control, and you might experiment with functionality exposed through them in partners' applications just as if it was your own service. (However, it doesn't automatically mean that you might draw some profits from having this control; for example, if you're allowing inserting pictures by their direct URL, your control over this integration is rather negligible, so it's generally better to provide those kinds of integration that allow having more control over the functionality in partners' apps.)
|
||||
|
||||
**NB**. While developing a “vertical” range of APIs, following the principles stated in the [“On the Waterline of the Iceberg”](#back-compat-iceberg-waterline) chapter is crucial. You might manipulate widget content and behavior if, and only if, developers can't “escape the sandbox,” i.e., have direct access to low-level objects encapsulated within the widget.
|
||||
**NB**. While developing a “vertical” range of APIs, following the principles stated in the “[On the Waterline of the Iceberg](#back-compat-iceberg-waterline)” chapter is crucial. You might manipulate widget content and behavior if, and only if, developers can't “escape the sandbox,” i.e., have direct access to low-level objects encapsulated within the widget.
|
||||
|
||||
In general, you should aim to have each partner using the API services in a manner that maximizes your profit as an API vendor. Where the partner doesn't try to make some unique experience and needs just a typical solution, you would benefit from making them use widgets, which are under your full control and thus ease the API version fragmentation problem and allow for experimenting in order to reach your KPIs. Where the partner possesses some unique expertise in the subject area and develops a unique service on top of your API, you would benefit from allowing full freedom in customizing the integration, so they might cover specific market niches and enjoy the advantage of offering more flexibility compared to services using competing APIs.
|
@ -21,7 +21,7 @@ Generally speaking, there are two approaches we might take, the static one and t
|
||||
|
||||
As both static and behavioral analyses are heuristic, it's highly desirable to not make decisions based solely on their outcome but rather ask the suspicious users to additionally prove they're making legitimate requests. If such a mechanism is in place, the quality of an anti-fraud system will be dramatically improved, as it allows for increasing system sensitivity and enabling pro-active defense, e.g., asking users to pass the tests in advance.
|
||||
|
||||
In the case of services for end users, the main method of acquiring the second factor is redirecting to a captcha page. In the case of APIs it might be problematic, especially if you initially neglected the “Stipulate Restrictions” rule we've given in the [“Describing Final Interfaces”](#api-design-describing-interfaces) chapter. In many cases, you will have to impose this responsibility on partners (i.e., it will be partners who show captchas and identify users based on the signals received from the API endpoints). This will, of course, significantly impair the convenience of working with the API.
|
||||
In the case of services for end users, the main method of acquiring the second factor is redirecting to a captcha page. In the case of APIs it might be problematic, especially if you initially neglected the “Stipulate Restrictions” rule we've given in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter. In many cases, you will have to impose this responsibility on partners (i.e., it will be partners who show captchas and identify users based on the signals received from the API endpoints). This will, of course, significantly impair the convenience of working with the API.
|
||||
|
||||
**NB**. Instead of captcha, there might be other actions introducing additional authentication factors. It might be the phone number confirmation or the second step of the 3D-Secure protocol. The important part is that requesting an additional authentication step must be stipulated in the program interface, as it can't be added later in a backwards-compatible manner.
|
||||
|
||||
|
@ -24,7 +24,7 @@ There are several options for tackling these issues:
|
||||
|
||||
2. The inverse scenario: partners must pay for technical support, and it's the API developers who answer the questions. It doesn't actually make a significant difference in terms of the quality of the issues (it's still mostly inexperienced developers who can't solve the problem on their own; you will just cut off those who can't afford paid support) but at least you won't have a hiring problem as you might allow yourself the luxury of having engineers for the first line of support.
|
||||
|
||||
3. Partly (or, sometimes, fully) the developer community might help with solving the amateur problems (see the [“Communicating with Developers”](#api-product-devrel) chapter). Usually, community members are pretty capable of answering those questions, especially if moderators help them.
|
||||
3. Partly (or, sometimes, fully) the developer community might help with solving the amateur problems (see the “[Communicating with Developers](#api-product-devrel)” chapter). Usually, community members are pretty capable of answering those questions, especially if moderators help them.
|
||||
|
||||
Importantly, whatever options you choose, it's still the API developers in the second line of support simply because only they can fully understand the problem and the partners' code. That implies two important consequences:
|
||||
|
||||
|
@ -6,7 +6,7 @@ Finally, the last aspect we would like to shed the light on is managing partners
|
||||
|
||||
Ideally, the API once published should live eternally; but as we all are reasonable people, we do understand it's impossible in the real life. Even if we continue supporting older versions, they will still become outdated eventually, and partners will need to rewrite the code to use newer functionality.
|
||||
|
||||
The author of this book formulates the rule of issuing new major API versions like this: the period of time after which partners will need to rewrite the code should coincide with the application lifespan in the subject area (see [“The Backward Compatibility Problem Statement”](#back-compat-statement) chapter). Apart from updating *major* versions, sooner or later you will face issues with accessing some outdated *minor* versions as well. As we mentioned in the [“On the Waterline of the Iceberg”](#back-compat-iceberg-waterline) chapter, even fixing bugs might eventually lead to breaking some integrations, and that naturally leads us to the necessity of keeping older *minor* versions of the API until the partner resolves the problem.
|
||||
The author of this book formulates the rule of issuing new major API versions like this: the period of time after which partners will need to rewrite the code should coincide with the application lifespan in the subject area (see “[The Backward Compatibility Problem Statement](#back-compat-statement)” chapter). Apart from updating *major* versions, sooner or later you will face issues with accessing some outdated *minor* versions as well. As we mentioned in the “[On the Waterline of the Iceberg](#back-compat-iceberg-waterline)” chapter, even fixing bugs might eventually lead to breaking some integrations, and that naturally leads us to the necessity of keeping older *minor* versions of the API until the partner resolves the problem.
|
||||
|
||||
In this aspect, integrating with large companies that have a dedicated software engineering department differs dramatically from providing a solution to individual amateur programmers: on one hand, the former are much more likely to find undocumented features and unfixed bugs in your code; on the other hand, because of the internal bureaucracy, fixing the related issues might easily take months, save not years. The common recommendation there is to maintain old minor API versions for a period of time long enough for the most dilatory partner to switch no the newest version.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user