mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-02-16 18:34:31 +02:00
massive build refactoring
This commit is contained in:
parent
d6f1466175
commit
ff968089ae
BIN
docs/API.en.epub
BIN
docs/API.en.epub
Binary file not shown.
590
docs/API.en.html
590
docs/API.en.html
File diff suppressed because one or more lines are too long
BIN
docs/API.en.pdf
BIN
docs/API.en.pdf
Binary file not shown.
BIN
docs/API.ru.epub
BIN
docs/API.ru.epub
Binary file not shown.
756
docs/API.ru.html
756
docs/API.ru.html
File diff suppressed because one or more lines are too long
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
Binary file not shown.
BIN
docs/assets/RobotoMono-VariableFont_wght.ttf
Normal file
BIN
docs/assets/RobotoMono-VariableFont_wght.ttf
Normal file
Binary file not shown.
@ -22,7 +22,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: local-monospace;
|
||||
src: url(../assets/RobotoMono-Regular.ttf);
|
||||
src: url(../assets/RobotoMono-VariableFont_wght.ttf);
|
||||
}
|
||||
|
||||
body {
|
||||
@ -31,5 +31,5 @@ body {
|
||||
|
||||
pre,
|
||||
code {
|
||||
font-family: local-sans;
|
||||
font-family: local-monospace;
|
||||
}
|
@ -1,14 +1,3 @@
|
||||
@font-face {
|
||||
font-family: local-serif;
|
||||
src: url(PTSerif-Regular.ttf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: local-serif;
|
||||
src: url(PTSerif-Bold.ttf);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@ -6,63 +6,89 @@
|
||||
“The API” book by Sergey Konstantinov. Examples to the “Decomposing
|
||||
UI Components” chapter.
|
||||
</title>
|
||||
<link rel="stylesheet" type="text/css" href="../fonts.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../assets/fonts.css" />
|
||||
<link rel="icon" type="image/png" href="../../assets/favicon.png" />
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="../../assets/monaco-editor/dev/vs/loader.js"
|
||||
></script>
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
<style>
|
||||
body {
|
||||
padding: 5px;
|
||||
:root {
|
||||
--example-list-width: 200px;
|
||||
--live-example-width: 390px;
|
||||
}
|
||||
|
||||
header * {
|
||||
font-size: 16px;
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
padding-bottom: 10px;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
ul#example-list {
|
||||
list-style-type: none;
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0 0 5px 0;
|
||||
}
|
||||
ul#example-list > li {
|
||||
margin: 2px 5px;
|
||||
padding: 3px;
|
||||
border: 2px solid rgba(128, 128, 128, 0.3);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
ul#example-list > li > a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
ul#example-list > li.selected {
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
#examples {
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
#examples #editor {
|
||||
min-height: 500px;
|
||||
min-width: 400px;
|
||||
width: calc(100% - 400px);
|
||||
float: left;
|
||||
#playground {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#live-example {
|
||||
min-width: 390px;
|
||||
width: var(--live-example-width);
|
||||
}
|
||||
|
||||
#examples {
|
||||
display: flex;
|
||||
width: calc(100% - var(--live-example-width) - 16px);
|
||||
}
|
||||
|
||||
#example-list {
|
||||
width: var(--example-list-width);
|
||||
font-size: 80%;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#example-list li {
|
||||
border: 2px solid rgba(80, 80, 80, 0.2);
|
||||
margin: 4px 0;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#example-list li.selected {
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
#example-list a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#editor {
|
||||
min-width: 0 !important;
|
||||
min-height: 500px;
|
||||
float: left;
|
||||
width: calc(100% - var(--example-list-width));
|
||||
float: none;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
#live-example iframe {
|
||||
@ -90,7 +116,7 @@
|
||||
<p>
|
||||
<a
|
||||
href="https://twirl.github.io/The-API-Book/API.en.html#chapter-44"
|
||||
>Chapter 43: Problems of Introducing UI Components</a
|
||||
>Chapter 44: Problems of Introducing UI Components</a
|
||||
>. Find the source code of the components and additional tasks
|
||||
for self-study at the
|
||||
<a
|
||||
|
@ -28,9 +28,9 @@ window.onload = function () {
|
||||
}
|
||||
iframe = document.createElement('iframe');
|
||||
iframe.srcdoc = `<!doctype html><html><head>
|
||||
<link rel="stylesheet" type="text/css" href="../fonts.css" />
|
||||
<link rel="stylesheet" type="text/css" href="./style.css" />
|
||||
<link rel="icon" type="image/png" href="../../assets/favicon.png" />
|
||||
<link rel="stylesheet" type="text/css" href="../../assets/fonts.css" />
|
||||
<link rel="stylesheet" type="text/css" href="./style.css" />
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
<style>
|
||||
html, body {width: 100%; height: 100%; margin: 0; padding: 0; background-color: #F9F9F9;}
|
||||
|
@ -26,6 +26,7 @@
|
||||
property="og:url"
|
||||
content="https://github.com/twirl/The-API-Book"
|
||||
/>
|
||||
<link rel="stylesheet" href="assets/fonts.css"/>
|
||||
<link rel="stylesheet" href="assets/landing.css"/>
|
||||
</head>
|
||||
<body>
|
||||
@ -40,7 +41,7 @@
|
||||
<br />Subscribe for updates on <a class="github" href="https://github.com/twirl/The-API-Book"></a>
|
||||
<br/>Follow me on <a class="linkedin" href="https://www.linkedin.com/in/twirl/"></a> · <a class="twitter" href="https://twitter.com/blogovodoved"></a> · <a class="substack" href="https://twirl.substack.com/">Substack</a>
|
||||
<br />Support this work on <a class="patreon" href="https://www.patreon.com/yatwirl">Patreon</a> · <a class="kindle" href="https://www.amazon.com/gp/product/B09RHH44S5/ref=dbs_a_def_rwt_hsch_vapi_tkin_p1_i0">buy Kindle version [1st edition]</a>
|
||||
<br />Share: <a class="share share-facebook" href="https://www.facebook.com/sharer.php?u=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2F" target="_blank"></a> · <a class="share share-twitter" href="https://twitter.com/intent/tweet?text=The%20API%20by%20Sergey%20Konstantinov%20%E2%80%94%20a%20book%20about%20designing%20APIs%2C%20extending%20them%20and%20finding%20a%20proper%20place%20in%20the%20market&url=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2F&hashtags=API%2CTheAPIBook&via=blogovodoved" target="_blank"></a> · <a class="share share-linkedin" href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2F" target="_blank"></a> · <a class="share share-reddit" href="http://www.reddit.com/submit?url=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2F&title=The%20API%20by%20Sergey%20Konstantinov%20%E2%80%94%20a%20book%20about%20designing%20APIs%2C%20extending%20them%20and%20finding%20a%20proper%20place%20in%20the%20market" target="_blank"></a><br/>⚙️⚙️⚙️
|
||||
<br />Share: <a class="share share-facebook" href="https://www.facebook.com/sharer.php?u=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2F" target="_blank"></a> · <a class="share share-twitter" href="https://twitter.com/intent/tweet?text=The%20API%20by%20Sergey%20Konstantinov%20%E2%80%94%20a%20book%20about%20designing%20APIs%2C%20extending%20them%20and%20finding%20a%20proper%20place%20in%20the%20market&url=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2F&hashtags=API%2CTheAPIBook&via=blogovodoved" target="_blank"></a> · <a class="share share-linkedin" href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2F" target="_blank"></a> · <a class="share share-reddit" href="https://www.reddit.com/submit?url=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2F&title=The%20API%20by%20Sergey%20Konstantinov%20%E2%80%94%20a%20book%20about%20designing%20APIs%2C%20extending%20them%20and%20finding%20a%20proper%20place%20in%20the%20market" target="_blank"></a><br/>⚙️⚙️⚙️
|
||||
</nav>
|
||||
<p>API-first development is one of the hottest technical topics nowadays since many companies have started to realize that APIs serves as a multiplier to their opportunities — but it amplifies the design mistakes as well.</p>
|
||||
<p>This book is written to share expertise and describe best practices in designing and developing APIs. It comprises six sections dedicated to the following topics:</p>
|
||||
@ -125,7 +126,7 @@
|
||||
<li><a href="API.en.html#sdk-toc">Chapter 41. On the Content of This Section</a></li>
|
||||
<li><a href="API.en.html#sdk-problems-solutions">Chapter 42. SDKs: Problems and Solutions</a></li>
|
||||
<li><a href="API.en.html#sdk-ui-components">Chapter 43. Problems of Introducing UI Components</a></li>
|
||||
<li><a href="API.en.html#chapter-44">Chapter 44. Decomposing UI Components</a></li>
|
||||
<li><a href="API.en.html#sdk-decomposing">Chapter 44. Decomposing UI Components</a></li>
|
||||
<li><a href="API.en.html#chapter-45">Chapter 45. The MV* Frameworks</a></li>
|
||||
<li><a href="API.en.html#chapter-46">Chapter 46. The Backend-Driven UI</a></li>
|
||||
<li><a href="API.en.html#chapter-47">Chapter 47. Shared Resources and Asynchronous Locks</a></li>
|
||||
@ -157,11 +158,11 @@
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p>This book is distributed under the <a href="http://creativecommons.org/licenses/by-nc/4.0/">Creative Commons Attribution-NonCommercial 4.0 International licence</a>.</p>
|
||||
<p>This book is distributed under the <a href="https://creativecommons.org/licenses/by-nc/4.0/">Creative Commons Attribution-NonCommercial 4.0 International licence</a>.</p>
|
||||
<p>Source code available at <a href="https://github.com/twirl/The-API-Book">github.com/twirl/The-API-Book</a></p>
|
||||
<h3><a name="about-author">About the Author</a></h3>
|
||||
<section class="about-me">
|
||||
<aside><img src="https://konstantinov.cc/static/me.png"/><br/>Photo by <a href="http://linkedin.com/in/zloylos/">Denis Hananein</a></aside>
|
||||
<aside><img src="https://konstantinov.cc/static/me.png"/><br/>Photo by <a href="https://linkedin.com/in/zloylos/">Denis Hananein</a></aside>
|
||||
<div class="content">
|
||||
<p>Sergey Konstantinov has been working with APIs for over a decade. He began his career as a software engineer in the Maps API division at Yandex and eventually became the head of the service. In this role, he was responsible for both technical architecture and product management.</p>
|
||||
<p>During this tenure, Sergey gained unique experience in building world-class APIs with a daily audience of tens of millions, planning roadmaps for such a service, and delivering numerous public speeches. Additionaly, he served as a member of the W3C Technical Architecture Group for a year and a half.</p>
|
||||
|
@ -26,6 +26,7 @@
|
||||
property="og:url"
|
||||
content="https://github.com/twirl/The-API-Book"
|
||||
/>
|
||||
<link rel="stylesheet" href="assets/fonts.css"/>
|
||||
<link rel="stylesheet" href="assets/landing.css"/>
|
||||
</head>
|
||||
<body>
|
||||
@ -40,7 +41,7 @@
|
||||
<br />Подпишитесь на обновления на <a class="habr" href="https://habr.com/ru/users/forgotten/">Хабре</a>
|
||||
<br/>Follow me on <a class="linkedin" href="https://www.linkedin.com/in/twirl/"></a> · <a class="twitter" href="https://twitter.com/blogovodoved"></a> · <a class="substack" href="https://twirl.substack.com/">Substack</a>
|
||||
<br />Поддержите эту работу на <a class="patreon" href="https://www.patreon.com/yatwirl">Patreon</a>
|
||||
<br />Поделиться: <a class="share share-facebook" href="https://www.facebook.com/sharer.php?u=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2Findex.ru.html" target="_blank"></a> · <a class="share share-twitter" href="https://twitter.com/intent/tweet?text=%C2%ABAPI%C2%BB%20%D0%A1%D0%B5%D1%80%D0%B3%D0%B5%D1%8F%20%D0%9A%D0%BE%D0%BD%D1%81%D1%82%D0%B0%D0%BD%D1%82%D0%B8%D0%BD%D0%BE%D0%B2%D0%B0%20%E2%80%94%20%D0%BA%D0%BD%D0%B8%D0%B3%D0%B0%20%D0%BE%20%D0%B4%D0%B8%D0%B7%D0%B0%D0%B9%D0%BD%D0%B5%20API%20%D0%B8%20%D0%B5%D0%B3%D0%BE%20%D0%BF%D1%80%D0%BE%D0%B4%D1%83%D0%BA%D1%82%D0%BE%D0%B2%D0%BE%D0%BC%20%D0%B8%20%D1%82%D0%B5%D1%85%D0%BD%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%BC%20%D1%80%D0%B0%D0%B7%D0%B2%D0%B8%D1%82%D0%B8%D0%B8&url=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2Findex.ru.html&hashtags=API%2CTheAPIBook&via=blogovodoved" target="_blank"></a> · <a class="share share-linkedin" href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2Findex.ru.html" target="_blank"></a> · <a class="share share-reddit" href="http://www.reddit.com/submit?url=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2Findex.ru.html&title=%C2%ABAPI%C2%BB%20%D0%A1%D0%B5%D1%80%D0%B3%D0%B5%D1%8F%20%D0%9A%D0%BE%D0%BD%D1%81%D1%82%D0%B0%D0%BD%D1%82%D0%B8%D0%BD%D0%BE%D0%B2%D0%B0%20%E2%80%94%20%D0%BA%D0%BD%D0%B8%D0%B3%D0%B0%20%D0%BE%20%D0%B4%D0%B8%D0%B7%D0%B0%D0%B9%D0%BD%D0%B5%20API%20%D0%B8%20%D0%B5%D0%B3%D0%BE%20%D0%BF%D1%80%D0%BE%D0%B4%D1%83%D0%BA%D1%82%D0%BE%D0%B2%D0%BE%D0%BC%20%D0%B8%20%D1%82%D0%B5%D1%85%D0%BD%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%BC%20%D1%80%D0%B0%D0%B7%D0%B2%D0%B8%D1%82%D0%B8%D0%B8" target="_blank"></a><br/>⚙️⚙️⚙️
|
||||
<br />Поделиться: <a class="share share-facebook" href="https://www.facebook.com/sharer.php?u=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2Findex.ru.html" target="_blank"></a> · <a class="share share-twitter" href="https://twitter.com/intent/tweet?text=%C2%ABAPI%C2%BB%20%D0%A1%D0%B5%D1%80%D0%B3%D0%B5%D1%8F%20%D0%9A%D0%BE%D0%BD%D1%81%D1%82%D0%B0%D0%BD%D1%82%D0%B8%D0%BD%D0%BE%D0%B2%D0%B0%20%E2%80%94%20%D0%BA%D0%BD%D0%B8%D0%B3%D0%B0%20%D0%BE%20%D0%B4%D0%B8%D0%B7%D0%B0%D0%B9%D0%BD%D0%B5%20API%20%D0%B8%20%D0%B5%D0%B3%D0%BE%20%D0%BF%D1%80%D0%BE%D0%B4%D1%83%D0%BA%D1%82%D0%BE%D0%B2%D0%BE%D0%BC%20%D0%B8%20%D1%82%D0%B5%D1%85%D0%BD%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%BC%20%D1%80%D0%B0%D0%B7%D0%B2%D0%B8%D1%82%D0%B8%D0%B8&url=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2Findex.ru.html&hashtags=API%2CTheAPIBook&via=blogovodoved" target="_blank"></a> · <a class="share share-linkedin" href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2Findex.ru.html" target="_blank"></a> · <a class="share share-reddit" href="https://www.reddit.com/submit?url=https%3A%2F%2Ftwirl.github.io%2FThe-API-Book%2Findex.ru.html&title=%C2%ABAPI%C2%BB%20%D0%A1%D0%B5%D1%80%D0%B3%D0%B5%D1%8F%20%D0%9A%D0%BE%D0%BD%D1%81%D1%82%D0%B0%D0%BD%D1%82%D0%B8%D0%BD%D0%BE%D0%B2%D0%B0%20%E2%80%94%20%D0%BA%D0%BD%D0%B8%D0%B3%D0%B0%20%D0%BE%20%D0%B4%D0%B8%D0%B7%D0%B0%D0%B9%D0%BD%D0%B5%20API%20%D0%B8%20%D0%B5%D0%B3%D0%BE%20%D0%BF%D1%80%D0%BE%D0%B4%D1%83%D0%BA%D1%82%D0%BE%D0%B2%D0%BE%D0%BC%20%D0%B8%20%D1%82%D0%B5%D1%85%D0%BD%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%BC%20%D1%80%D0%B0%D0%B7%D0%B2%D0%B8%D1%82%D0%B8%D0%B8" target="_blank"></a><br/>⚙️⚙️⚙️
|
||||
</nav>
|
||||
<p>«API-first» подход — одна из самых горячих горячих тем в разработке программного обеспечения в наше время. Многие компании начали понимать, что API выступает мультипликатором их возможностей — но также умножает и допущенные ошибки.</p>
|
||||
<p>Эта книга написана для того, чтобы поделиться опытом и изложить лучшие практики разработки API. Книга состоит из шести разделов, посвящённых:</p>
|
||||
@ -125,7 +126,7 @@
|
||||
<li><a href="API.ru.html#sdk-toc">Глава 41. О содержании раздела</a></li>
|
||||
<li><a href="API.ru.html#sdk-problems-solutions">Глава 42. SDK: проблемы и решения</a></li>
|
||||
<li><a href="API.ru.html#sdk-ui-components">Глава 43. Проблемы встраивания UI-компонентов</a></li>
|
||||
<li><a href="API.ru.html#chapter-44">Глава 44. Декомпозиция UI-компонентов. MV*-подходы</a></li>
|
||||
<li><a href="API.ru.html#sdk-decomposing">Глава 44. Декомпозиция UI-компонентов</a></li>
|
||||
<li><a href="API.ru.html#chapter-45">Глава 45. MV*-фреймворки</a></li>
|
||||
<li><a href="API.ru.html#chapter-46">Глава 46. Backend-Driven UI</a></li>
|
||||
<li><a href="API.ru.html#chapter-47">Глава 47. Разделяемые ресурсы и асинхронные блокировки</a></li>
|
||||
@ -151,12 +152,17 @@
|
||||
<li><a href="API.ru.html#api-product-expectations">Глава 62. Управление ожиданиями</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h4>Интерактивные примеры</h3>
|
||||
<ul><li><a href="examples/01. Decomposing UI Components">Decomposing UI Components</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p>Это произведение доступно по <a href="http://creativecommons.org/licenses/by-nc/4.0/">лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция — Некоммерческое использование») 4.0 Всемирная</a>.</p>
|
||||
<p>Это произведение доступно по <a href="https://creativecommons.org/licenses/by-nc/4.0/">лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция — Некоммерческое использование») 4.0 Всемирная</a>.</p>
|
||||
<p>Исходный код доступен на <a href="https://github.com/twirl/The-API-Book">github.com/twirl/The-API-Book</a></p>
|
||||
<h3><a name="about-author">Об авторе</a></h3>
|
||||
<section class="about-me">
|
||||
<aside><img src="https://konstantinov.cc/static/me.png"/><br/>Фото: <a href="http://linkedin.com/in/zloylos/">Denis Hananein</a></aside>
|
||||
<aside><img src="https://konstantinov.cc/static/me.png"/><br/>Фото: <a href="https://linkedin.com/in/zloylos/">Denis Hananein</a></aside>
|
||||
<div class="content">
|
||||
<p>Сергей Константинов работает с API уже больше десятилетия. Он начинал свою карьеру разработчиком в подразделении API Яндекс.Карт, и со временем стал руководителем всего сервиса, отвечая и за техническую, и за продуктовую составляющую.</p>
|
||||
<p>За это время Сергей получил уникальный опыт построения API мирового уровня с дневной аудиторией в десятки миллионов человек, планирования роадмапов для такого продукта и многочисленных публичных выступлений. Он также проработал полтора года в составе Технической архитектурной группы W3C.</p>
|
||||
|
@ -7,7 +7,7 @@
|
||||
"version": "2.0.0",
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.6.1",
|
||||
"@twirl/book-builder": "0.0.23",
|
||||
"@twirl/book-builder": "0.0.24",
|
||||
"@types/jest": "^29.5.3",
|
||||
"express": "^4.18.2",
|
||||
"jest": "^29.6.1",
|
||||
|
@ -4,9 +4,18 @@ import { init, plugins } from '@twirl/book-builder';
|
||||
import templates from '../src/templates.js';
|
||||
import { buildLanding } from './build-landing.mjs';
|
||||
|
||||
if (process.argv[2] == '--clean') {
|
||||
const flags = process.argv.reduce((flags, v) => {
|
||||
switch (v) {
|
||||
case '--clean-cache':
|
||||
flags.cleanCache = true;
|
||||
break;
|
||||
}
|
||||
return flags;
|
||||
}, {});
|
||||
|
||||
if (flags.cleanCache) {
|
||||
console.log('Cleaning cache…');
|
||||
clean();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const l10n = {
|
||||
@ -33,71 +42,78 @@ const chapters = process.argv[4];
|
||||
const noCache = process.argv[5] == '--no-cache';
|
||||
|
||||
console.log(`Building langs: ${langsToBuild.join(', ')}…`);
|
||||
langsToBuild.forEach((lang) => {
|
||||
init({
|
||||
l10n: l10n[lang],
|
||||
basePath: pathResolve(`src`),
|
||||
path: pathResolve(`src/${lang}/clean-copy`),
|
||||
templates,
|
||||
pipeline: {
|
||||
css: {
|
||||
beforeAll: [
|
||||
plugins.css.backgroundImageDataUri,
|
||||
plugins.css.fontFaceDataUri
|
||||
]
|
||||
(async () => {
|
||||
for (const lang of langsToBuild) {
|
||||
await init({
|
||||
l10n: l10n[lang],
|
||||
basePath: pathResolve(`src`),
|
||||
path: pathResolve(`src/${lang}/clean-copy`),
|
||||
templates,
|
||||
pipeline: {
|
||||
css: {
|
||||
beforeAll: [
|
||||
plugins.css.backgroundImageDataUri,
|
||||
plugins.css.fontFaceDataUri
|
||||
]
|
||||
},
|
||||
ast: {
|
||||
preProcess: [
|
||||
plugins.ast.h3ToTitle,
|
||||
plugins.ast.h5Counter,
|
||||
plugins.ast.aImg,
|
||||
plugins.ast.imgSrcResolve,
|
||||
plugins.ast.highlighter({
|
||||
languages: ['javascript', 'typescript']
|
||||
}),
|
||||
plugins.ast.ref,
|
||||
plugins.ast.ghTableFix,
|
||||
plugins.ast.stat
|
||||
]
|
||||
},
|
||||
htmlSourceValidator: {
|
||||
validator: 'WHATWG',
|
||||
ignore: [
|
||||
'heading-level',
|
||||
'no-raw-characters',
|
||||
'wcag/h37',
|
||||
'no-missing-references'
|
||||
]
|
||||
},
|
||||
html: {
|
||||
postProcess: [plugins.html.imgDataUri]
|
||||
}
|
||||
},
|
||||
ast: {
|
||||
preProcess: [
|
||||
plugins.ast.h3ToTitle,
|
||||
plugins.ast.h5Counter,
|
||||
plugins.ast.aImg,
|
||||
plugins.ast.imgSrcResolve,
|
||||
plugins.ast.highlighter({
|
||||
languages: ['javascript', 'typescript']
|
||||
}),
|
||||
plugins.ast.ref,
|
||||
plugins.ast.ghTableFix,
|
||||
plugins.ast.stat
|
||||
]
|
||||
},
|
||||
htmlSourceValidator: {
|
||||
validator: 'WHATWG',
|
||||
ignore: [
|
||||
'heading-level',
|
||||
'no-raw-characters',
|
||||
'wcag/h37',
|
||||
'no-missing-references'
|
||||
]
|
||||
},
|
||||
html: {
|
||||
postProcess: [plugins.html.imgDataUri]
|
||||
}
|
||||
},
|
||||
chapters,
|
||||
noCache
|
||||
}).then((builder) => {
|
||||
Object.keys(targets).forEach((target) => {
|
||||
if (target !== 'landing') {
|
||||
builder.build(
|
||||
target,
|
||||
pathResolve('docs', `${l10n[lang].file}.${lang}.${target}`)
|
||||
);
|
||||
console.log(
|
||||
`Finished lang=${lang} target=${target}\n${Object.entries({
|
||||
sources: 'Sources',
|
||||
references: 'references',
|
||||
words: 'words',
|
||||
characters: 'characters'
|
||||
})
|
||||
.map(([k, v]) => `${v}: ${builder.structure[k]}`)
|
||||
.join(', ')}`
|
||||
);
|
||||
} else {
|
||||
buildLanding(builder.structure, lang, l10n, templates);
|
||||
chapters,
|
||||
noCache
|
||||
}).then(async (builder) => {
|
||||
for (const target of Object.keys(targets)) {
|
||||
if (target !== 'landing') {
|
||||
await builder.build(
|
||||
target,
|
||||
pathResolve(
|
||||
'docs',
|
||||
`${l10n[lang].file}.${lang}.${target}`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
`Finished lang=${lang} target=${target}\n${Object.entries(
|
||||
{
|
||||
sources: 'Sources',
|
||||
references: 'references',
|
||||
words: 'words',
|
||||
characters: 'characters'
|
||||
}
|
||||
)
|
||||
.map(([k, v]) => `${v}: ${builder.structure[k]}`)
|
||||
.join(', ')}`
|
||||
);
|
||||
} else {
|
||||
buildLanding(builder.structure, lang, l10n, templates);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
function clean() {
|
||||
const tmpDir = pathResolve('.', '.tmp');
|
||||
|
@ -73,7 +73,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: local-monospace;
|
||||
src: url(/fonts/RobotoMono-Regular.ttf);
|
||||
src: url(/fonts/RobotoMono-VariableFont_wght.ttf);
|
||||
}
|
||||
|
||||
html,
|
||||
@ -84,6 +84,17 @@ body {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
sup,
|
||||
sub {
|
||||
vertical-align: baseline;
|
||||
position: relative;
|
||||
top: -0.4em;
|
||||
}
|
||||
|
||||
sub {
|
||||
top: 0.4em;
|
||||
}
|
||||
|
||||
.display-none {
|
||||
display: none;
|
||||
}
|
||||
@ -120,6 +131,12 @@ code {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
pre code em {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.img-wrapper {
|
||||
text-align: center;
|
||||
}
|
||||
@ -258,3 +275,60 @@ a.anchor {
|
||||
.hljs-comment {
|
||||
color: #655f6d;
|
||||
}
|
||||
|
||||
ul.references,
|
||||
ul.bibliography {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
a.ref sup,
|
||||
ul.references sup,
|
||||
ul.bibliography sup {
|
||||
font-size: 60%;
|
||||
}
|
||||
|
||||
ul.references li,
|
||||
ul.bibliography li {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
ul.references a,
|
||||
ul.references a:hover {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dashed;
|
||||
}
|
||||
|
||||
a.ref,
|
||||
ul.bibliography a,
|
||||
ul.bibliography a:hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
ul.references a.external,
|
||||
ul.bibliography a.external,
|
||||
ul.bibliography a.external:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: solid;
|
||||
font-size: 70%;
|
||||
word-break: break-all;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
ul.references a.external.text {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
ul.references li p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul.references li a.back-anchor {
|
||||
text-decoration: none;
|
||||
vertical-align: top;
|
||||
text-align: right;
|
||||
padding-right: 0.3em;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ Still, two main scenarios dominate the stage when we talk about API development:
|
||||
* Developing client-server applications
|
||||
* Developing client SDKs.
|
||||
|
||||
In the first case, we almost universally talk about APIs working atop the HTTP protocol. Today, the only notable examples of non-HTTP-based client-server interaction protocols are WebSocket (though it might, and frequently does, work in conjunction with HTTP), MQTT, and highly specialized APIs like media streaming and broadcasting formats.
|
||||
In the first case, we almost universally talk about APIs working atop the HTTP protocol. Today, the only notable examples of non-HTTP-based client-server interaction protocols are *WebSocket* (though it might, and frequently does, work in conjunction with HTTP), *MQTT*, and highly specialized APIs like media streaming and broadcasting formats.
|
||||
|
||||
#### HTTP API
|
||||
|
||||
@ -16,19 +16,19 @@ Although the technology looks homogeneous because of using the same application-
|
||||
* Either the client-server interaction heavily relies on the features described in the HTTP standard (or rather standards, as the functionality is split across several different RFCs), or
|
||||
* HTTP is used as transport, and there is an additional abstraction level built upon it (i.e., the HTTP capabilities, such as the headers and status codes nomenclatures, are deliberately reduced to a bare minimum, and all the metadata is handled by the higher-level protocol).
|
||||
|
||||
The APIs that belong to the first category are usually denoted as “REST” or “RESTful” APIs. The second category comprises mostly different RPC formats.
|
||||
The APIs that belong to the first category are usually denoted as “*REST*” or “*RESTful*” APIs. The second category comprises mostly protocols for making remote procedure calls (RPC).
|
||||
|
||||
**Second**, different HTTP APIs rely on different data formats:
|
||||
* REST APIs and some RPCs ([JSON-RPC](https://www.jsonrpc.org/), [GraphQL](https://graphql.org/), etc.) use the [JSON](https://www.ecma-international.org/publications-and-standards/standards/ecma-404/) format (sometimes with some additional endpoints to transfer binary data)
|
||||
* [gRPC](https://grpc.io/) and some specialized RPC protocols like [Apache Avro](https://avro.apache.org/docs/) utilize binary formats (such as [Protocol Buffers](https://protobuf.dev/), [FlatBuffers](https://flatbuffers.dev/), or Apache Avro's own format)
|
||||
* Finally, some RPC protocols (notably [SOAP](https://www.w3.org/TR/soap12/) and [XML-RPC](http://xmlrpc.com/)) employ the [XML](https://www.w3.org/TR/xml/) data format (which is considered a rather outdated practice by many developers).
|
||||
* REST APIs and some RPC protocols (such as *JSON-RPC*[ref JSON-RPC](https://www.jsonrpc.org/), *GraphQL*[ref GraphQL](https://graphql.org/), etc.) use the *JSON*[ref JSON](https://www.ecma-international.org/publications-and-standards/standards/ecma-404/) format (sometimes with some additional endpoints to transfer binary data)
|
||||
* *gRPC*[ref gRPC](https://grpc.io/) and some specialized RPC protocols like *Apache Avro*[ref Apache Avro](https://avro.apache.org/docs/) utilize binary formats (such as *Protocol Buffers*[ref Protocol Buffers](https://protobuf.dev/), *FlatBuffers*[ref FlatBuffers](https://flatbuffers.dev/), or *Apache Avro*'s own format)
|
||||
* Finally, some RPC protocols (notably *SOAP*[ref SOAP](https://www.w3.org/TR/soap12/) and *XML-RPC*[ref XML-RPC](http://xmlrpc.com/)) employ the *XML*[ref Extensible Markup Language (XML)](https://www.w3.org/TR/xml/) data format (which is considered a rather outdated practice by many developers).
|
||||
|
||||
All the above-mentioned technologies operate in significantly dissimilar paradigms, which give rise to rather hot “holy war” debates among software engineers. However, at the moment this book is being written we observe the choice for general-purpose APIs is reduced to the “REST API (in fact, JSON-over-HTTP) vs. gRPC vs. GraphQL” triad.
|
||||
All the above-mentioned technologies operate in significantly dissimilar paradigms, which give rise to rather hot “holy war” debates among software engineers. However, at the moment this book is being written we observe the choice for general-purpose APIs is reduced to the “*REST API* (in fact, JSON-over-HTTP) vs. *gRPC* vs. *GraphQL*” triad.
|
||||
|
||||
#### SDKs
|
||||
|
||||
The term “SDK” (stands for “Software Development Kit”) is not, strictly speaking, related to APIs: this is a generic term for a software toolkit. As with “REST,” however, it got some popular reading as a client framework to work with some underlying API. This might be, for example, a wrapper to a client-server API or a UI to some OS API. The major difference from the APIs we discussed in the previous paragraph is that an “SDK” is implemented for a specific programming language and platform to work with some underlying low-level API.
|
||||
|
||||
Unlike client-server APIs, such SDKs can hardly be generalized as each of them is developed for a specific language-platform pair. Interoperable SDKs exist, notably cross-platform mobile ([React Native](https://reactnative.dev/), [Flutter](https://flutter.dev/), [Xamarin](https://dotnet.microsoft.com/en-us/apps/xamarin), etc.) and desktop ([JavaFX](https://openjfx.io/), [QT](https://www.qt.io/), etc.) frameworks and some highly-specialized solutions ([Unity](https://docs.unity3d.com/Manual/index.html)). However, they are still narrowly focused on concrete technologies.
|
||||
Unlike client-server APIs, such SDKs can hardly be generalized as each of them is developed for a specific language-platform pair. Interoperable SDKs exist, notably cross-platform mobile (*React Native*[ref React Native](https://reactnative.dev/), *Flutter*[ref Flutter](https://flutter.dev/), *Xamarin*[ref Xamarin](https://dotnet.microsoft.com/en-us/apps/xamarin), etc.) and desktop (*JavaFX*[ref JavaFX](https://openjfx.io/), *QT*[ref QT](https://www.qt.io/), etc.) frameworks and some highly-specialized solutions (*Unity*[ref Unity](https://docs.unity3d.com/Manual/index.html)). However, they are still narrowly focused on concrete technologies.
|
||||
|
||||
Still, SDKs feature some generality in terms of *the problems they solve*, and Section V of this book will be dedicated to solving these problems of translating contexts and making UI components.
|
@ -1,6 +1,6 @@
|
||||
### [On Versioning][intro-versioning]
|
||||
|
||||
Here and throughout this book, we firmly adhere to [semver](https://semver.org/) principles of versioning.
|
||||
Here and throughout this book, we firmly adhere to the Semantic Versioning (*semver*)[ref Semantic Versioning 2.0.0](https://semver.org/) principles:
|
||||
|
||||
1. API versions are denoted with three numbers, e.g., `1.2.3`.
|
||||
2. The first number (a major version) increases when backward-incompatible changes in the API are introduced.
|
||||
|
@ -213,7 +213,7 @@ It is also worth mentioning that unresolvable errors are useless to a user at th
|
||||
|
||||
From our own API development experience, we can tell without a doubt that the greatest final interface design mistake (and the greatest developer's pain accordingly) is the excessive overloading of entities' interfaces with fields, methods, events, parameters, and other attributes.
|
||||
|
||||
Meanwhile, there is the “Golden Rule” of interface design (applicable not only to APIs but almost to anything): humans can comfortably keep 7±2 entities in short-term memory. Manipulating a larger number of chunks complicates things for most humans. The rule is also known as [Miller's Law](https://en.wikipedia.org/wiki/Working_memory#Capacity).
|
||||
Meanwhile, there is the “Golden Rule” of interface design (applicable not only to APIs but almost to anything): humans can comfortably keep 7±2 entities in short-term memory. Manipulating a larger number of chunks complicates things for most humans. The rule is also known as Miller's Law[ref Miller's Law](https://en.wikipedia.org/wiki/Working_memory#Capacity).
|
||||
|
||||
The only possible method of overcoming this law is decomposition. Entities should be grouped under a single designation at every concept level of the API so that developers never have to operate on more than a reasonable amount of entities (let's say, ten) at a time.
|
||||
|
||||
|
@ -193,7 +193,7 @@ Improving these function signatures is left as an exercise for the reader.
|
||||
**Better**: `"prohibit_calling": true` or `"avoid_calling": true`
|
||||
— this is easier to read. However, you should not deceive yourself: it is still a double negation, even if you've found a “negative” word without a “negative” prefix.
|
||||
|
||||
It is also worth mentioning that mistakes in using [De Morgan's laws](https://en.wikipedia.org/wiki/De_Morgan's_laws) are even more common. For example, if you have two flags:
|
||||
It is also worth mentioning that mistakes in using De Morgan's laws[ref De Morgan's laws](https://en.wikipedia.org/wiki/De_Morgan's_laws) are even more common. For example, if you have two flags:
|
||||
|
||||
```
|
||||
GET /coffee-machines/{id}/stocks
|
||||
@ -529,7 +529,7 @@ POST /v1/coffee-machines/search
|
||||
}
|
||||
```
|
||||
|
||||
Formally speaking, having such behavior is feasible: why not have a “default geographical coordinates” concept? However, in reality, such policies of “silently” fixing mistakes lead to absurd situations like “the null island” — [the most visited place in the world](https://www.sciencealert.com/welcome-to-null-island-the-most-visited-place-that-doesn-t-exist). The more popular an API becomes, the higher the chances that partners will overlook these edge cases.
|
||||
Formally speaking, having such behavior is feasible: why not have a “default geographical coordinates” concept? However, in reality, such policies of “silently” fixing mistakes lead to absurd situations like “the null island” — the most visited place in the world[ref Hrala, J. Welcome to Null Island, The Most 'Visited' Place on Earth That Doesn't Actually Exist](https://www.sciencealert.com/welcome-to-null-island-the-most-visited-place-that-doesn-t-exist). The more popular an API becomes, the higher the chances that partners will overlook these edge cases.
|
||||
|
||||
**Better**:
|
||||
```
|
||||
@ -869,11 +869,11 @@ However, be warned: clients are bad at implementing idempotency tokens. Two comm
|
||||
|
||||
If the author of this book were given a dollar each time he had to implement an additional security protocol invented by someone, he would be retired by now. API developers' inclination to create new signing procedures for requests or complex schemes of exchanging passwords for tokens is both obvious and meaningless.
|
||||
|
||||
**First**, there is no need to reinvent the wheel when it comes to security-enhancing procedures for various operations. All the algorithms you need are already invented, just adopt and implement them. No self-invented algorithm for request signature checking can provide the same level of protection against a [Man-in-the-Middle attack](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) as a TLS connection with mutual certificate pinning.
|
||||
**First**, there is no need to reinvent the wheel when it comes to security-enhancing procedures for various operations. All the algorithms you need are already invented, just adopt and implement them. No self-invented algorithm for request signature checking can provide the same level of protection against a Manipulator-in-the-middle (*MitM*) attack[ref Manipulator-in-the-middle Attack](https://owasp.org/www-community/attacks/Manipulator-in-the-middle_attack) as a mutual TLS authentication with certificate pinning[ref Mutual Authentication. mTLS](https://en.wikipedia.org/wiki/Mutual_authentication#mTLS).
|
||||
|
||||
**Second**, assuming oneself to be an expert in security is presumptuous and dangerous. New attack vectors emerge daily, and staying fully aware of all actual threats is a full-time job. If you do something different during workdays, the security system you design will contain vulnerabilities that you have never heard about — for example, your password-checking algorithm might be susceptible to a [timing attack](https://en.wikipedia.org/wiki/Timing_attack) or your webserver might be vulnerable to a [request splitting attack](https://capec.mitre.org/data/definitions/105.html).
|
||||
**Second**, assuming oneself to be an expert in security is presumptuous and dangerous. New attack vectors emerge daily, and staying fully aware of all actual threats is a full-time job. If you do something different during workdays, the security system you design will contain vulnerabilities that you have never heard about — for example, your password-checking algorithm might be susceptible to a timing attack[ref Timing Attack](https://en.wikipedia.org/wiki/Timing_attack) or your webserver might be vulnerable to a request splitting attack[ref HTTP Request Splitting](https://capec.mitre.org/data/definitions/105.html).
|
||||
|
||||
The OWASP Foundation compiles a list of the most common vulnerabilities in APIs every year, which we strongly recommend studying.
|
||||
The OWASP Foundation compiles a list of the most common vulnerabilities in APIs every year,[ref OWASP API Security Project](https://owasp.org/www-project-api-security/) which we strongly recommend studying.
|
||||
|
||||
And just in case: all APIs must be provided over TLS 1.2 or higher (preferably 1.3).
|
||||
|
||||
@ -951,7 +951,7 @@ In the second case, you will be able to sanitize parameters and avoid SQL inject
|
||||
|
||||
##### Use Globally Unique Identifiers
|
||||
|
||||
It's considered good practice to use globally unique strings as entity identifiers, either semantic (e.g., "lungo" for beverage types) or random ones (e.g., [UUID-4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random))). It might turn out to be extremely useful if you need to merge data from several sources under a single identifier.
|
||||
It's considered good practice to use globally unique strings as entity identifiers, either semantic (e.g., "lungo" for beverage types) or random ones (e.g., UUID-4[ref Universally Unique Identifier. Version 4 (random)](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random))). It might turn out to be extremely useful if you need to merge data from several sources under a single identifier.
|
||||
|
||||
In general, we tend to advise using URN-like identifiers, e.g. `urn:order:<uuid>` (or just `order:<uuid>`). That helps a lot in dealing with legacy systems with different identifiers attached to the same entity. Namespaces in URNs help to quickly understand which identifier is used and if there is a usage mistake.
|
||||
|
||||
|
@ -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” in the field of software engineering was introduced by Kent Beck and Ward Cunningham in 1987[ref Software Design Pattern. History](https://en.wikipedia.org/wiki/Software_design_pattern#History) 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[ref:{"short":"Gamma, E., Helm, R., Johnson, R., Vlissides, J. (1994)","extra":["Design Patterns","Elements of Reusable Object-Oriented Software"]}](isbn:9780321700698). 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.
|
||||
|
||||
|
@ -13,7 +13,7 @@ Let's proceed to the technical problems that API developers face. We begin with
|
||||
4. The server finally gets the initial request for creating an order and serves it.
|
||||
5. The client, being unaware of this, tries to create an order anew.
|
||||
|
||||
As the operations of reading the list of ongoing orders and of creating a new order happen at different moments of time, we can't guarantee that the system state hasn't changed in between. If we do want to have this guarantee, we must implement some [synchronization strategy](https://en.wikipedia.org/wiki/Synchronization_(computer_science)). In the case of, let's say, operating system APIs or client frameworks we might rely on the primitives provided by the platform. But in the case of distributed client-server APIs, we would need to implement such a primitive of our own.
|
||||
As the operations of reading the list of ongoing orders and of creating a new order happen at different moments of time, we can't guarantee that the system state hasn't changed in between. If we do want to have this guarantee, we must implement some synchronization strategy[ref Synchronization (Computer Science)](https://en.wikipedia.org/wiki/Synchronization_(computer_science)). In the case of, let's say, operating system APIs or client frameworks we might rely on the primitives provided by the platform. But in the case of distributed client-server APIs, we would need to implement such a primitive of our own.
|
||||
|
||||
There are two main approaches to solving this problem: the pessimistic one (implementing locks in the API) and the optimistic one (resource versioning).
|
||||
|
||||
@ -51,13 +51,13 @@ try {
|
||||
Rather unsurprisingly, this approach sees very rare use in distributed client-server APIs because of the plethora of related problems:
|
||||
|
||||
1. Waiting for acquiring a lock introduces new latencies to the interaction that are hardly predictable and might potentially be quite significant.
|
||||
2. The lock itself is one more entity that constitutes a subsystem of its own, and quite a demanding one as [strong consistency](https://en.wikipedia.org/wiki/Strong_consistency) is required for implementing locks: the `getPendingOrders` function must return the up-to-date state of the system otherwise the duplicate order will be anyway created.
|
||||
2. The lock itself is one more entity that constitutes a subsystem of its own, and quite a demanding one as strong consistency[ref Strong consistency](https://en.wikipedia.org/wiki/Strong_consistency) is required for implementing locks: the `getPendingOrders` function must return the up-to-date state of the system otherwise the duplicate order will be anyway created.
|
||||
3. As it's partners who develop client code, we can't guarantee it works with locks always correctly. Inevitably, “lost” locks will occur in the system, and that means we need to provide some tools to partners so they can find the problem and debug it.
|
||||
4. A certain granularity of locks is to be developed so that partners can't affect each other. We are lucky if there are natural boundaries for a lock — for example, if it's limited to a specific user in the specific partner's system. If we are not so lucky (let's say all partners share the same user profile), we will have to develop even more complex systems to deal with potential errors in the partners' code — for example, introduce locking quotas.
|
||||
|
||||
#### Optimistic Concurrency Control
|
||||
|
||||
A less implementation-heavy approach is to develop an [optimistic concurrency control](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) system, i.e., to require clients to pass a flag proving they know the actual state of a shared resource.
|
||||
A less implementation-heavy approach is to develop an optimistic concurrency control[ref Optimistic concurrency control](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) system, i.e., to require clients to pass a flag proving they know the actual state of a shared resource.
|
||||
|
||||
```
|
||||
// Retrieve the state
|
||||
@ -83,7 +83,7 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
**NB**: An attentive reader might note that the necessity to implement some synchronization strategy and strongly consistent reading has not disappeared: there must be a component in the system that performs a locking read of the resource version and its subsequent change. It's not entirely true as synchronization strategies and strongly consistent reading have disappeared *from the public API*. The distance between the client that sets the lock and the server that processes it became much smaller, and the entire interaction now happens in a controllable environment. It might be a single subsystem in a form of [an ACID-compatible database](https://en.wikipedia.org/wiki/ACID) or even an in-memory solution.
|
||||
**NB**: An attentive reader might note that the necessity to implement some synchronization strategy and strongly consistent reading has not disappeared: there must be a component in the system that performs a locking read of the resource version and its subsequent change. It's not entirely true as synchronization strategies and strongly consistent reading have disappeared *from the public API*. The distance between the client that sets the lock and the server that processes it became much smaller, and the entire interaction now happens in a controllable environment. It might be a single subsystem in the form of an ACID-compatible[ref ACID](https://en.wikipedia.org/wiki/ACID) database or even an in-memory solution.
|
||||
|
||||
Instead of a version, the date of the last modification of the resource might be used (which is much less reliable as clocks are not ideally synchronized across different system nodes; at least save it with the maximum possible precision!) or entity identifiers (ETags).
|
||||
|
||||
|
@ -20,7 +20,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.
|
||||
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[ref Consistency Model. 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.
|
||||
|
||||
@ -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[ref Consistency Model. Read-Your-Writes Consistency](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
|
||||
|
@ -56,7 +56,7 @@ 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
|
||||
* 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” technique[ref Token Bucket](https://en.wikipedia.org/wiki/Token_bucket)
|
||||
* 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.
|
||||
|
@ -227,7 +227,7 @@ The first request format allows for implementing the first scenario, i.e., retri
|
||||
|
||||
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.
|
||||
* If the storage supports parallel writing to several nodes, the most recently created record might have a slightly earlier creation date than the second-recent one because clocks on different nodes might tick slightly differently, and it is challenging to achieve even microsecond-precision coherence[[1]](https://www.yugabyte.com/blog/evolving-clock-sync-for-distributed-databases/). This breaks the monotonicity invariant, which makes it poorly fit for use in public APIs. If there is no other choice but relying on such storage, one of two evils is to be chosen:
|
||||
* If the storage supports parallel writing to several nodes, the most recently created record might have a slightly earlier creation date than the second-recent one because clocks on different nodes might tick slightly differently, and it is challenging to achieve even microsecond-precision coherence.[ref Ranganathan, K. A Matter of Time: Evolving Clock Sync for Distributed Databases](https://www.yugabyte.com/blog/evolving-clock-sync-for-distributed-databases/) This breaks the monotonicity invariant, which makes it poorly fit for use in public APIs. If there is no other choice but relying on such storage, one of two evils is to be chosen:
|
||||
* Introducing artificial delays, i.e., returning only items created earlier than N seconds ago, selecting this N to be certainly less than the clock irregularity. This technique also works in the case of asynchronously populated lists. Keep in mind, however, that this solution is probabilistic, and wrong data will be served to clients in case of backend synchronization problems.
|
||||
* Describe the instability of ordering list items in the docs (and thus make partners responsible for solving arising issues).
|
||||
|
||||
|
@ -15,14 +15,14 @@ GET /v1/orders/created-history⮠
|
||||
}
|
||||
```
|
||||
|
||||
This pattern (known as [*polling*](https://en.wikipedia.org/wiki/Polling_(computer_science))) is the most common approach to organizing two-way communication in an API when a partner needs not only to send data to the server but also to receive notifications from the server about changes in some state.
|
||||
This pattern (known as *polling*[ref Polling (Computer Science)](https://en.wikipedia.org/wiki/Polling_(computer_science))) is the most common approach to organizing two-way communication in an API when a partner needs not only to send data to the server but also to receive notifications from the server about changes in some state.
|
||||
|
||||
Although this approach is quite easy to implement, polling always requires a compromise between responsiveness, performance, and system throughput:
|
||||
|
||||
* The longer the interval between consecutive requests, the greater the delay between the change of state on the server and receiving the information about it on the client, and the potentially larger the traffic volume that needs to be transmitted in one iteration.
|
||||
* On the other hand, the shorter this interval, the more requests will be made in vain, as no changes in the system have occurred during the elapsed time.
|
||||
|
||||
In other words, polling always generates some background traffic in the system but never guarantees maximum responsiveness. Sometimes, this problem is solved by using the so-called “[long polling](https://en.wikipedia.org/wiki/Push_technology#Long_polling),” which intentionally delays the server's response for a prolonged period (seconds, tens of seconds) until some state change occurs. However, we do not recommend using this approach in modern systems due to associated technical problems, particularly in unreliable network conditions where the client has no way of knowing that the connection is lost, and a new request needs to be sent.
|
||||
In other words, polling always generates some background traffic in the system but never guarantees maximum responsiveness. Sometimes, this problem is solved by using the so-called “long polling[ref Push Technology. Long Polling](https://en.wikipedia.org/wiki/Push_technology#Long_polling),” which intentionally delays the server's response for a prolonged period (seconds, tens of seconds) until some state change occurs. However, we do not recommend using this approach in modern systems due to associated technical problems, particularly in unreliable network conditions where the client has no way of knowing that the connection is lost, and a new request needs to be sent.
|
||||
|
||||
If regular polling is insufficient to solve the user's problem, you can switch to a reverse model (push) in which the server itself informs the client that changes have occurred in the system.
|
||||
|
||||
@ -36,19 +36,19 @@ Three alternatives to polling might be proposed:
|
||||
|
||||
##### Duplex Connections
|
||||
|
||||
The most obvious option is to use technologies that can transmit messages in both directions over a single connection. The best-known example of such technology is [WebSocket](https://websockets.spec.whatwg.org/). Sometimes, [the Server Push functionality of the HTTP/2 protocol](https://datatracker.ietf.org/doc/html/rfc7540#section-8.2) is used for this purpose; however, we must note that the specification formally does not allow such usage. There is also the [WebRTC](https://www.w3.org/TR/webrtc/) protocol; its main purpose is a peer-to-peer exchange of media data, and it's rarely used in client-server interaction.
|
||||
The most obvious option is to use technologies that can transmit messages in both directions over a single connection. The best-known example of such technology is *WebSockets*[ref WebSockets](https://websockets.spec.whatwg.org/). Sometimes, the Server Push functionality of the HTTP/2 protocol[ref Hypertext Transfer Protocol Version 2 (HTTP/2). Server Push](https://datatracker.ietf.org/doc/html/rfc7540#section-8.2) is used for this purpose; however, we must note that the specification formally does not allow such usage. There is also the *WebRTC*[ref WebRTC: Real-Time Communication in Browsers](https://www.w3.org/TR/webrtc/) protocol; its main purpose is a peer-to-peer exchange of media data, and it's rarely used in client-server interaction.
|
||||
|
||||
Although the idea looks simple and attractive, its applicability to real-world use cases is limited. Popular server software and frameworks do not support server-initiated message sending (gRPC does support it, but the client should initiate the exchange; using gRPC server streams to send server-initiated events is essentially employing HTTP/2 server pushes for this purpose, and it's the same technique as in the long polling approach, just a bit more modern), and the existing specification definition standards do not support it — as WebSocket is a low-level protocol, and you will need to design the interaction format on your own.
|
||||
Although the idea looks simple and attractive, its applicability to real-world use cases is limited. Popular server software and frameworks do not support server-initiated message sending (for instance, gRPC does support streamed responses[ref gRPC. Server streaming RPC](https://grpc.io/docs/what-is-grpc/core-concepts/#server-streaming-rpc), but the client should initiate the exchange; using gRPC server streams to send server-initiated events is essentially employing HTTP/2 server pushes for this purpose, and it's the same technique as in the long polling approach, just a bit more modern), and the existing specification definition standards do not support it — as WebSocket is a low-level protocol, and you will need to design the interaction format on your own.
|
||||
|
||||
Duplex connections still suffer from the unreliability of the network and require implementing additional tricks to tell the difference between a network problem and the absence of new messages. All these issues result in limited applicability of the technology; it's mostly used in web applications.
|
||||
|
||||
##### Separate Callback Channels
|
||||
|
||||
Instead of a duplex connection, two separate connections might be used: one for sending requests to the server and one to receive notifications from the server. The most popular technology of this kind is [MQTT](https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html). Although it is considered very effective because of utilizing low-level protocols, its disadvantages follow from its advantages:
|
||||
Instead of a duplex connection, two separate connections might be used: one for sending requests to the server and one to receive notifications from the server. The most popular technology of this kind is *MQTT*[ref MQTT](https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html). Although it is considered very effective because of utilizing low-level protocols, its disadvantages follow from its advantages:
|
||||
* The technology is meant to implement the pub/sub pattern, and its main value is that the server software (MQTT Broker) is provided alongside the protocol itself. Applying it to other tasks, especially bidirectional communication, might be challenging.
|
||||
* The low-level protocols force you to develop your own data formats.
|
||||
|
||||
There is also a Web standard for sending server notifications called [Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html) (SSE). However, it's less functional than WebSocket (only text data and unidirectional flow are allowed) and rarely used.
|
||||
There is also a Web standard for sending server notifications called Server-Sent Events[ref HTML Living Standard. Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html) (SSE). However, it's less functional than WebSocket (only text data and unidirectional flow are allowed) and rarely used.
|
||||
|
||||
##### Third-Party Push Notifications
|
||||
|
||||
@ -89,7 +89,7 @@ What is important is that the *must* be a formal contract (preferably in a form
|
||||
|
||||
##### 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.
|
||||
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[ref Mutual Authentication. 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.
|
||||
|
||||
##### 3. Develop an Interface for Setting the URL of a *Webhook*
|
||||
|
||||
@ -98,7 +98,7 @@ As the callback endpoint is developed by partners, we do not know its URL before
|
||||
**Importantly**, the operation of setting a *webhook* 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:
|
||||
* By setting an arbitrary URL, the perpetrator might get access to all partner's orders (and the partner might lose access)
|
||||
* This vulnerability might be used for organizing DoS attacks on third parties
|
||||
* If an internal URL might be set as a *webhook*, a [SSRF attack](https://en.wikipedia.org/wiki/SSRF) might be directed toward the API vendor's own infrastructure.
|
||||
* If an internal URL might be set as a *webhook*, a SSRF attack[ref SSRF](https://en.wikipedia.org/wiki/SSRF) might be directed toward the API vendor's own infrastructure.
|
||||
|
||||
#### Typical Problems of *Webhook*-Powered Integrations
|
||||
|
||||
@ -116,7 +116,7 @@ Obviously, we can't guarantee partners don't make any of these mistakes. The onl
|
||||
1. The system state must be restorable. If the partner erroneously responded that messages are processed while they are not, there must be a possibility for them to redeem themselves and get the list of missed events and/or the full system state and fix all the issues
|
||||
2. Help partners to write proper code by describing in the documentation all unobvious subtleties that inexperienced developers might be unaware of:
|
||||
* Idempotency keys for every operation
|
||||
* Delivery guarantees (“at least once,” “exactly ones,” etc.; see the [reference description](https://docs.confluent.io/kafka/design/delivery-semantics.html) on the example of Apache Kafka API)
|
||||
* Delivery guarantees (“at least once,” “exactly ones,” etc.; see the reference description[ref Apache Kafka. Kafka Design. Message Delivery Guarantees](https://docs.confluent.io/kafka/design/delivery-semantics.html) on the example of *Apache Kafka* API)
|
||||
* Possibility of the server generating parallel requests and the maximum number of such requests at a time
|
||||
* Guarantees of message ordering (i.e., the notifications are always delivered ordered from the oldest one to the newest one) or the absence of such guarantees
|
||||
* The sizes of all messages and message fields in bytes
|
||||
@ -127,9 +127,9 @@ Obviously, we can't guarantee partners don't make any of these mistakes. The onl
|
||||
|
||||
#### Message Queues
|
||||
|
||||
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.
|
||||
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[ref Web Services 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 switching any inter-service communication to message queues.
|
||||
To solve these problems, and also to ensure better horizontal scalability, message queues[ref Message Queue](https://en.wikipedia.org/wiki/Message_queue) were developed, most notably numerous pub/sub pattern[ref Publish / Subscribe Pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_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
|
||||
|
@ -188,7 +188,7 @@ POST /v1/bulk-status-change
|
||||
If sub-operations in the list depend on each other (as in the example above: the partner needs *both* refunding and canceling the order to succeed as there is no sense to fulfill only one of them) or the execution order is important, non-atomic endpoints will constantly lead to new problems. And if you think that in your subject area, there are no such problems, it might turn out at any moment that you have overlooked something.
|
||||
|
||||
So, our recommendations for bulk modifying endpoints are:
|
||||
1. If you can avoid creating such endpoints — do it. In server-to-server integrations, the profit is marginal. In modern networks that support [QUIC](https://datatracker.ietf.org/doc/html/rfc9000) and request multiplexing, it's also dubious.
|
||||
1. If you can avoid creating such endpoints — do it. In server-to-server integrations, the profit is marginal. In modern networks that support QUIC[ref QUIC: A UDP-Based Multiplexed and Secure Transport](https://datatracker.ietf.org/doc/html/rfc9000) and request multiplexing, it's also dubious.
|
||||
2. If you can not, make the endpoint atomic and provide SDKs to help partners avoid typical mistakes.
|
||||
3. If implementing an atomic endpoint is not possible, elaborate on the API design thoroughly, keeping in mind the caveats we discussed.
|
||||
4. Whichever option you choose, it is crucially important to include a breakdown of the sub-requests in the response. For atomic endpoints, this entails ensuring that the error message contains a list of errors that prevented the request execution, ideally encompassing the potential errors as well (i.e., the results of validity checks for all the sub-requests). For non-atomic endpoints, it means returning a list of statuses corresponding to each sub-request along with errors that occurred during the execution.
|
||||
|
@ -94,7 +94,7 @@ However, upon closer examination all these conclusions seem less viable:
|
||||
* Finally, the naïve approach of organizing collaborative editing by allowing conflicting operations to be carried out if they don't touch the same fields works only if the changes are transitive. In our case, they are not: the result of simultaneously removing the first element in the list and editing the second one depends on the execution order.
|
||||
* Often, developers try to reduce the outgoing traffic volume as well by returning an empty server response for modifying operations. Therefore, two clients editing the same entity do not see the changes made by each other until they explicitly refresh the state, which further increases the chance of yielding highly unexpected results.
|
||||
|
||||
The solution could be enhanced by introducing explicit control sequences instead of relying on “magical” values and adding meta settings for the operation (such as a field name filter as it's [implemented in gRPC](https://protobuf.dev/reference/protobuf/google.protobuf/#field-masks-updates)). Here's an example:
|
||||
The solution could be enhanced by introducing explicit control sequences instead of relying on “magical” values and adding meta settings for the operation (such as a field name filter as it's implemented in gRPC over Protobuf[ref Protocol Buffers. Field Masks in Update Operations](https://protobuf.dev/reference/protobuf/google.protobuf/#field-masks-updates)). Here's an example:
|
||||
|
||||
```
|
||||
// Partially rewrites the order:
|
||||
@ -233,4 +233,4 @@ X-Idempotency-Token: <token>
|
||||
|
||||
This approach is much more complex to implement, but it is the only viable technique for realizing collaborative editing as it explicitly reflects the exact actions the client applied to an entity. Having the changes in this format also allows for organizing offline editing with accumulating changes on the client side for the server to resolve the conflict later based on the revision history.
|
||||
|
||||
**NB**: One approach to this task is developing a set of operations in which all actions are transitive (i.e., the final state of the entity does not change regardless of the order in which the changes were applied). One example of such a nomenclature is [CRDT](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type). However, we consider this approach viable only in some subject areas, as in real life, non-transitive changes are always possible. If one user entered new text in the document and another user removed the document completely, there is no way to automatically resolve this conflict that would satisfy both users. The only correct way of resolving this conflict is explicitly asking users which option for mitigating the issue they prefer.
|
||||
**NB**: One approach to this task is developing a set of operations in which all actions are transitive (i.e., the final state of the entity does not change regardless of the order in which the changes were applied). One example of such a nomenclature is a conflict-free replicated data type (*CRDT*).[ref Conflict-Free Replicated Data Type](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) However, we consider this approach viable only in some subject areas, as in real life, non-transitive changes are always possible. If one user entered new text in the document and another user removed the document completely, there is no way to automatically resolve this conflict that would satisfy both users. The only correct way of resolving this conflict is explicitly asking users which option for mitigating the issue they prefer.
|
@ -6,7 +6,7 @@ Let us summarize what we have written in the three previous chapters:
|
||||
2. Higher-level entities are to be the informational contexts for low-level ones, meaning they don't prescribe any specific behavior but rather translate their state and expose functionality to modify it, either directly through calling some methods or indirectly through firing events.
|
||||
3. Concrete functionality, such as working with “bare metal” hardware or underlying platform APIs, should be delegated to low-level entities.
|
||||
|
||||
**NB**: There is nothing novel about these rules: one might easily recognize them as the [SOLID](https://en.wikipedia.org/wiki/SOLID) architecture principles. This is not surprising either, as SOLID focuses on contract-oriented development, and APIs are contracts by definition. We have simply introduced the concepts of “abstraction levels” and “informational contexts” to these principles.
|
||||
**NB**: There is nothing novel about these rules: one might easily recognize them as the *SOLID* architecture principles[ref SOLID](https://en.wikipedia.org/wiki/SOLID)[ref:{"short":"Martin, R. C.","extra":["Design Principles and Design Patterns"]}](http://staff.cs.utu.fi/~jounsmed/doos_06/material/DesignPrinciplesAndPatterns.pdf). This is not surprising either, as *SOLID* focuses on contract-oriented development, and APIs are contracts by definition. We have simply introduced the concepts of “abstraction levels” and “informational contexts” to these principles.
|
||||
|
||||
However, there remains an unanswered question: how should we design the entity nomenclature from the beginning so that extending the API won't result in a mess of assorted inconsistent methods from different stages of development? The answer is quite obvious: to avoid clumsy situations during abstracting (as with the recipe properties), all the entities must be originally considered as specific implementations of a more general interface, even if there are no planned alternative implementations for them.
|
||||
|
||||
|
@ -8,7 +8,7 @@ However, from a practical standpoint, there is a significant inconvenience that
|
||||
* Firstly, humans are not adept at remembering IP addresses and prefer readable names
|
||||
* Secondly, an IP address is a technical entity bound to a specific network node while developers require the ability to add or modify nodes without having to modify the code of their applications.
|
||||
|
||||
The domain name system, which allows for assigning human-readable aliases to IP addresses, has proved to be a convenient abstraction with almost universal adoption. Introducing domain names necessitated the development of new protocols at a higher level than TCP/IP. For text (hypertext) data this protocol happened to be [HTTP 0.9](https://www.w3.org/Protocols/HTTP/AsImplemented.html) developed by Tim Berners-Lee and published in 1991. Besides enabling the use of network node names, HTTP also provided another useful abstraction: assigning separate addresses to endpoints working on the same network node.
|
||||
The domain name system, which allows for assigning human-readable aliases to IP addresses, has proved to be a convenient abstraction with almost universal adoption. Introducing domain names necessitated the development of new protocols at a higher level than TCP/IP. For text (hypertext) data this protocol happened to be HTTP 0.9[ref The Original HTTP as defined in 1991](https://www.w3.org/Protocols/HTTP/AsImplemented.html) developed by Tim Berners-Lee and published in 1991. Besides enabling the use of network node names, HTTP also provided another useful abstraction: assigning separate addresses to endpoints working on the same network node.
|
||||
|
||||
Initially, the protocol was very simple and merely described a method of retrieving a document by establishing a TCP/IP connection to the server and passing a string in the `GET document_address` format. Subsequently, the protocol was enhanced by the URL standard for document addresses. After that, the protocol evolved rapidly: new verbs, response statuses, headers, data types, and other features emerged in a short time.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
### [Advantages and Disadvantages of HTTP APIs Compared to Alternative Technologies][http-api-pros-and-cons]
|
||||
|
||||
After reviewing the previous chapter, the reader may wonder why this dichotomy exists in the first place, i.e., why do some HTTP APIs rely on HTTP semantics, while others reject it in favor of custom arrangements, and still others are stuck somewhere in between? For example, if we consider the [JSON-RPC response format](https://www.jsonrpc.org/specification#response_object), we quickly notice that it could be replaced with standard HTTP protocol functionality. Instead of this:
|
||||
After reviewing the previous chapter, the reader may wonder why this dichotomy exists in the first place, i.e., why do some HTTP APIs rely on HTTP semantics, while others reject it in favor of custom arrangements, and still others are stuck somewhere in between? For example, if we consider the JSON-RPC response format,[ref JSON-RPC 2.0 Specification. Response object](https://www.jsonrpc.org/specification#response_object) we quickly notice that it could be replaced with standard HTTP protocol functionality. Instead of this:
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
@ -69,11 +69,11 @@ When discussing the advantages of alternative technologies such as GraphQL, gRPC
|
||||
|
||||
Let's be honest: HTTP APIs do suffer from the listed problems. However, we can confidently say that the impact of these factors is often overestimated. The reason API vendors care little about HTTP API performance is that the actual overhead is not as significant as perceived. Specifically:
|
||||
|
||||
1. Regarding the verbosity of the format, it is important to note that these issues are mainly relevant when compresiion algorithms are not utilized. [Comparisons](https://nilsmagnus.github.io/post/proto-json-sizes/) have shown that enabling compression algorithms such as gzip largely reduces the difference in sizes between JSON documents and alternative binary formats (and there are compression algorithms specifically designed for processing text data, such as [brotli](https://datatracker.ietf.org/doc/html/rfc7932)).
|
||||
1. Regarding the verbosity of the format, it is important to note that these issues are mainly relevant when compresiion algorithms are not utilized. Comparisons have shown[ref Comparing sizes of protobuf vs json](https://nilsmagnus.github.io/post/proto-json-sizes/) that enabling compression algorithms such as *gzip* largely reduces the difference in sizes between JSON documents and alternative binary formats (and there are compression algorithms specifically designed for processing text data, such as *brotli*[ref Brotli Compressed Data Format](https://datatracker.ietf.org/doc/html/rfc7932)).
|
||||
|
||||
2. If necessary, API designers can customize the list of returned fields in HTTP APIs. It aligns well with both the letter and the spirit of the standard. However, as we already explained to the reader in the “[Partial Updates](#api-patterns-partial-updates)” chapter, trying to minimize traffic by returning only subsets of data is rarely justified in well-designed APIs.
|
||||
|
||||
3. If standard JSON deserializers are used, the overhead compared to binary standards might indeed be significant. However, if this overhead is a real problem, it makes sense to consider alternative JSON serializers such as [simdjson](https://github.com/simdjson/simdjson). Due to their low-level and highly optimized code, simdjson demonstrates impressive throughput which would be suitable for all APIs except some corner cases.
|
||||
3. If standard JSON deserializers are used, the overhead compared to binary standards might indeed be significant. However, if this overhead is a real problem, it makes sense to consider alternative JSON serializers such as *simdjson*[ref simdjson : Parsing gigabytes of JSON per second](https://github.com/simdjson/simdjson). Due to their low-level and highly optimized code, *simdjson* demonstrates impressive throughput which would be suitable for all APIs except some corner cases.
|
||||
|
||||
4. Generally speaking, the HTTP API paradigm implies that binary data (such as images or video files) is served through separate endpoints. Returning binary data in JSON is only necessary when a separate request for the data is a problem from the performance perspective. These situations are virtually non-existent in server-to-server interactions and/or if HTTP/2 or a higher protocol version is used.
|
||||
|
||||
@ -83,7 +83,7 @@ Let us reiterate once more: JSON-over-HTTP APIs are *indeed* less performative t
|
||||
|
||||
#### Advantages and Disadvantages of the JSON Format
|
||||
|
||||
It's not hard to notice that most of the claims regarding HTTP API performance are actually not about the HTTP protocol but the JSON format. There is no problem in developing an HTTP API that will utilize any binary format (including, for instance, [Protocol Buffers](https://protobuf.dev/)). Then the difference between a Protobuf-over-HTTP API and a gRPC API would be just about using granular URLs, status codes, request / response headers, and the ensuing (in)ability to use integrated software tools out of the box.
|
||||
It's not hard to notice that most of the claims regarding HTTP API performance are actually not about the HTTP protocol but the JSON format. There is no problem in developing an HTTP API that will utilize any binary format (including, for instance, *Protocol Buffers*). Then the difference between a Protobuf-over-HTTP API and a gRPC API would be just about using granular URLs, status codes, request / response headers, and the ensuing (in)ability to use integrated software tools out of the box.
|
||||
|
||||
However, on many occasions (including this book) developers prefer the textual JSON over binary Protobuf (Flatbuffers, Thrift, Avro, etc.) for a very simple reason: JSON is easy to read. First, it's a text format and doesn't require additional decoding. Second, it's self-descriptive, meaning that property names are included. Unlike Protobuf-encoded messages which are basically impossible to read without a `.proto` file, one can make a very good guess as to what a JSON document is about at a glance. Provided that request metadata in HTTP APIs is readable as well, we ultimately get a communication format that is easy to parse and understand with just our eyes.
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Before we proceed to discuss HTTP API design patterns, we feel obliged to clarify one more important terminological issue. Often, an API matching the description we gave in the “[On the HTTP API Concept and Terminology](#http-api-concepts)” chapter is called a “REST API” or a “RESTful API.” In this Section, we don't use any of these terms as it makes no practical sense.
|
||||
|
||||
What is “REST”? In 2000, Roy Fielding, one of the authors of the HTTP and URI specifications, published his doctoral dissertation titled “Architectural Styles and the Design of Network-based Software Architectures,” the fifth chapter of which was named “[Representational State Transfer (REST)](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm).”
|
||||
What is “REST”? In 2000, Roy Fielding, one of the authors of the HTTP and URI specifications, published his doctoral dissertation titled “Architectural Styles and the Design of Network-based Software Architectures,” the fifth chapter of which was named “Representational State Transfer (REST).[ref:{"short":"Fielding, R. (2000)","extra":["Architectural Styles and the Design of Network-based Software Architectures","Representational State Transfer (REST)"]}](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm)”
|
||||
|
||||
As anyone can attest by reading this chapter, it features a very much abstract overview of a distributed client-server architecture that is not bound to either HTTP or URL. Furthermore, it does not discuss any API design recommendations. In this chapter, Fielding methodically *enumerates restrictions* that any software engineer encounters when developing distributed client-server software. Here they are:
|
||||
* The client and the server do not know how each of them is implemented
|
||||
@ -21,20 +21,20 @@ Consider the following:
|
||||
* If there is a uniform communication interface, it can be mimicked if needed, so the requirement of client and server implementation independence can always be met.
|
||||
* If we can create an alternative server, it means we can always have a layered architecture by placing an additional proxy between the client and the server.
|
||||
* As clients are computational machines, they *always* store some state and cache some data.
|
||||
* Finally, the code-on-demand requirement is a sly one as in a [von Neumann architecture](https://en.wikipedia.org/wiki/Von_Neumann_architecture), we can always say that the data the client receives actually comprises instructions in some formal language.
|
||||
* Finally, the code-on-demand requirement is a sly one as in a von Neumann architecture[ref Von Neumann Architecture](https://en.wikipedia.org/wiki/Von_Neumann_architecture), we can always say that the data the client receives actually comprises instructions in some formal language.
|
||||
|
||||
Yes, of course, the reasoning above is a sophism, a reduction to absurdity. Ironically, we might take the opposite path to absurdity by proclaiming that REST constraints are never met. For instance, the code-on-demand requirement obviously contradicts the requirement of having an independently-implemented client and server as the client must be able to interpret the instructions the server sends written in a specific language. As for the “S” rule (i.e., the “stateless” constraint), it is very hard to find a system that does not store *any* client context as it's close to impossible to make anything *useful* for the client in this case. (And, by the way, Fielding explicitly requires that: “communication … cannot take advantage of any stored context on the server.”)
|
||||
|
||||
Finally, in 2008, Fielding himself increased the entropy in the understanding of the concept by issuing a [clarification](https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven) explaining what he actually meant. In this article, among other things, he stated that:
|
||||
Finally, in 2008, Fielding himself increased the entropy in the understanding of the concept by issuing a clarification[ref Fielding, R. T. REST APIs must be hypertext-driven](https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven) explaining what he actually meant. In this article, among other things, he stated that:
|
||||
* REST API development must focus on describing media types representing resources
|
||||
* The client must be agnostic of these media types
|
||||
* There must not be fixed resource names and operations with resources. Clients must extract this information from the server's responses.
|
||||
|
||||
The concept of “Fielding-2008 REST” implies that clients, after somehow obtaining an entry point to the API, must be able to communicate with the server having no prior knowledge of the API and definitely must not contain any specific code to work with the API. This requirement is much stricter than the ones described in the dissertation of 2000. Particularly, REST-2008 implies that there are no fixed URL templates; actual URLs to perform operations with the resource are included in the resource representation (this concept is known as [HATEOAS](https://en.wikipedia.org/wiki/HATEOAS)). The dissertation of 2000 does not contain any definitions of “hypermedia” that contradict the idea of constructing such links based on the prior knowledge of the API (such as a specification).
|
||||
The concept of “Fielding-2008 REST” implies that clients, after somehow obtaining an entry point to the API, must be able to communicate with the server having no prior knowledge of the API and definitely must not contain any specific code to work with the API. This requirement is much stricter than the ones described in the dissertation of 2000. Particularly, REST-2008 implies that there are no fixed URL templates; actual URLs to perform operations with the resource are included in the resource representation (this concept is known as HATEOAS[ref HATEOAS](https://en.wikipedia.org/wiki/HATEOAS)). The dissertation of 2000 does not contain any definitions of “hypermedia” that contradict the idea of constructing such links based on the prior knowledge of the API (such as a specification).
|
||||
|
||||
**NB**: Leaving out the fact that Fielding rather loosely interpreted his own dissertation, let us point out that no system in the world complies with the Fielding-2008 definition of REST.
|
||||
|
||||
We have no idea why, out of all the overviews of abstract network-based software architecture, Fielding's concept gained such popularity. It is obvious that Fielding's theory, reflected in the minds of millions of software developers, became a genuine engineering subculture. By reducing the REST idea to the HTTP protocol and the URL standard, the chimera of a “RESTful API” was born, of which [nobody knows the definition](https://restfulapi.net/).
|
||||
We have no idea why, out of all the overviews of abstract network-based software architecture, Fielding's concept gained such popularity. It is obvious that Fielding's theory, reflected in the minds of millions of software developers, became a genuine engineering subculture. By reducing the REST idea to the HTTP protocol and the URL standard, the chimera of a “RESTful API” was born, of which nobody knows the definition.[ref Gupta, L. What is REST](https://restfulapi.net/)
|
||||
|
||||
Do we want to say that REST is a meaningful concept? Definitely not. We only aimed to explain that it allows for quite a broad range of interpretations, which is simultaneously its main power and its main weakness.
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
The important exercise we must conduct is to describe the format of an HTTP request and response and explain the basic concepts. Many of these may seem obvious to the reader. However, the situation is that even the basic knowledge we require to move further is scattered across vast and fragmented documentation, causing even experienced developers to struggle with some nuances. Below, we will try to compile a structured overview that is sufficient to design HTTP APIs.
|
||||
|
||||
To describe the semantics and formats, we will refer to the brand-new [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html), which replaces no fewer than nine previous specifications dealing with different aspects of the technology. However, a significant volume of additional functionality is still covered by separate standards. In particular, the HTTP caching principles are described in the standalone [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html), while the popular `PATCH` method is omitted in the main RFC and is regulated by [RFC 5789](https://www.rfc-editor.org/rfc/rfc5789.html).
|
||||
To describe the semantics and formats, we will refer to the brand-new RFC 9110[ref RFC 9110. HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html), which replaces no fewer than nine previous specifications dealing with different aspects of the technology. However, a significant volume of additional functionality is still covered by separate standards. In particular, the HTTP caching principles are described in the standalone RFC 9111[ref RFC 9111. HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111.html), while the popular `PATCH` method is omitted in the main RFC and is regulated by RFC 5789[ref PATCH Method for HTTP](https://www.rfc-editor.org/rfc/rfc5789.html).
|
||||
|
||||
An HTTP request consists of (1) applying a specific verb to a URL, stating (2) the protocol version, (3) additional meta-information in headers, and (4) optionally, some content (request body):
|
||||
|
||||
@ -33,13 +33,13 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
**NB**: In HTTP/2 (and future HTTP/3), separate binary frames are used for headers and data instead of the holistic text format. However, this doesn't affect the architectural concepts we will describe below. To avoid ambiguity, we will provide examples in the HTTP/1.1 format. You can find detailed information about the HTTP/2 format [here](https://hpbn.co/http2/).
|
||||
**NB**: In HTTP/2 (and future HTTP/3), separate binary frames are used for headers and data instead of the holistic text format.[ref:{"short":"Grigorik, I. (2013)","extra":["High Performance Browser Networking","Chapter 12. HTTP/2"]}](https://hpbn.co/http2/) However, this doesn't affect the architectural concepts we will describe below. To avoid ambiguity, we will provide examples in the HTTP/1.1 format.
|
||||
|
||||
##### A URL
|
||||
|
||||
A Uniform Resource Locator (URL) is an addressing unit in an HTTP API. Some evangelists of the technology even use the term “URL space” as a synonym for “The World Wide Web.” It is expected that a proper HTTP API should employ an addressing system that is as granular as the subject area itself; in other words, each entity that the API can manipulate should have its own URL.
|
||||
|
||||
The URL format is governed by a [separate standard](https://url.spec.whatwg.org/) developed by an independent body known as the Web Hypertext Application Technology Working Group (WHATWG). The concepts of URLs and Uniform Resource Names (URNs) together constitute a more general entity called Uniform Resource Identifiers (URIs). (The difference between the two is that a URL allows for *locating* a resource within the framework of some protocol whereas a URN is an “internal” entity name that does not provide information on how to find the resource.)
|
||||
The URL format is governed by a separate standard[ref URL Living Standard](https://url.spec.whatwg.org/) developed by an independent body known as the Web Hypertext Application Technology Working Group (WHATWG). The concepts of URLs and Uniform Resource Names (URNs) together constitute a more general entity called Uniform Resource Identifiers (URIs). (The difference between the two is that a URL allows for *locating* a resource within the framework of some protocol whereas a URN is an “internal” entity name that does not provide information on how to find the resource.)
|
||||
|
||||
URLs can be decomposed into sub-components, each of which is optional. While the standard enumerates a number of legacy practices, such as passing logins and passwords in URLs or using non-UTF encodings, we will skip discussing those. Instead, we will focus on the following components that are relevant to the topic of HTTP API design:
|
||||
* A scheme: a protocol to access the resource (in our case it is always `https:`)
|
||||
@ -68,7 +68,7 @@ Headers contain *metadata* associated with a request or a response. They might d
|
||||
|
||||
The important feature of headers is the possibility to read them before the message body is fully transmitted. This allows for altering request or response handling depending on the headers, and it is perfectly fine to manipulate headers while proxying requests. Many network agents actually do this, i.e., add, remove, or modify headers while proxying requests. In particular, modern web browsers automatically add a number of technical headers, such as `User-Agent`, `Origin`, `Accept-Language`, `Connection`, `Referer`, `Sec-Fetch-*`, etc., and modern server software automatically adds or modifies such headers as `X-Powered-By`, `Date`, `Content-Length`, `Content-Encoding`, `X-Forwarded-For`, etc.
|
||||
|
||||
This freedom in manipulating headers can result in unexpected problems if an API uses them to transmit data as the field names developed by an API vendor can accidentally overlap with existing conventional headers, or worse, such a collision might occur in the future at any moment. To avoid this issue, the practice of adding the prefix `X-` to custom header names was frequently used in the past. More than ten years ago this practice was officially discouraged (see the detailed overview in [RFC 6648](https://www.rfc-editor.org/rfc/rfc6648)). Nevertheless, the prefix has not been fully abolished, and many semi-standard headers still contain it (notably, `X-Forwarded-For`). Therefore, using the `X-` prefix reduces the probability of collision but does not eliminate it. The same RFC reasonably suggests using the API vendor name as a prefix instead of `X-`. (We would rather recommend using both, i.e., sticking to the `X-ApiName-FieldName` format. Here `X-` is included for readability [to distinguish standard fields from custom ones], and the company or API name part helps avoid collisions with other non-standard header names).
|
||||
This freedom in manipulating headers can result in unexpected problems if an API uses them to transmit data as the field names developed by an API vendor can accidentally overlap with existing conventional headers, or worse, such a collision might occur in the future at any moment. To avoid this issue, the practice of adding the prefix `X-` to custom header names was frequently used in the past. More than ten years ago this practice was officially discouraged (see the detailed overview in RFC 6648[ref Deprecating the "X-" Prefix and Similar Constructs in Application Protocols](https://www.rfc-editor.org/rfc/rfc6648)). Nevertheless, the prefix has not been fully abolished, and many semi-standard headers still contain it (notably, `X-Forwarded-For`). Therefore, using the `X-` prefix reduces the probability of collision but does not eliminate it. The same RFC reasonably suggests using the API vendor name as a prefix instead of `X-`. (We would rather recommend using both, i.e., sticking to the `X-ApiName-FieldName` format. Here `X-` is included for readability [to distinguish standard fields from custom ones], and the company or API name part helps avoid collisions with other non-standard header names).
|
||||
|
||||
Additionally, headers are used as control flow instructions for so-called “content negotiation,” which allows the client and server to agree on a response format (through `Accept*` headers) and to perform conditional requests that aim to reduce traffic by skipping response bodies, either fully or partially (through `If-*` headers, such as `If-Range`, `If-Modified-Since`, etc.)
|
||||
|
||||
@ -76,7 +76,7 @@ Additionally, headers are used as control flow instructions for so-called “con
|
||||
|
||||
One important component of an HTTP request is a method (verb) that describes the operation being applied to a resource. RFC 9110 standardizes eight verbs — namely, `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `CONNECT`, `OPTIONS`, and `TRACE` — of which we as API developers are interested in the former four. The `CONNECT`, `OPTIONS`, and `TRACE` methods are technical and rarely used in HTTP APIs (except for `OPTIONS`, which needs to be implemented to ensure access to the API from a web browser). Theoretically, the `HEAD` verb, which allows for requesting *resource metadata only*, might be quite useful in API design. However, for reasons unknown to us, it did not take root in this capacity.
|
||||
|
||||
Apart from RFC 9110, many other specifications propose additional HTTP verbs, such as `COPY`, `LOCK`, `SEARCH`, etc. — the full list can be found in [the registry](http://www.iana.org/assignments/http-methods/http-methods.xhtml). However, only one of them gained widespread popularity — the `PATCH` method. The reasons for this state of affairs are quite trivial: the five methods (`GET`, `POST`, `PUT`, `DELETE`, and `PATCH`) are enough for almost any API.
|
||||
Apart from RFC 9110, many other specifications propose additional HTTP verbs, such as `COPY`, `LOCK`, `SEARCH`, etc. — the full list can be found in the registry[ref Hypertext Transfer Protocol (HTTP) Method Registry](https://www.iana.org/assignments/http-methods/http-methods.xhtml). However, only one of them gained widespread popularity — the `PATCH` method. The reasons for this state of affairs are quite trivial: the five methods (`GET`, `POST`, `PUT`, `DELETE`, and `PATCH`) are enough for almost any API.
|
||||
|
||||
HTTP verbs define two important characteristics of an HTTP call:
|
||||
* Semantics: what the operation *means*
|
||||
@ -157,7 +157,7 @@ However, when we move a parameter around different components, we face three ann
|
||||
* With header *values*, there is even more chaos: some of them are required to be case-insensitive (e.g., `Content-Type`), while others are prescribed to be case-sensitive (e.g., `ETag`)
|
||||
* Allowed symbols and escaping rules differ as well:
|
||||
* Notably, there is no widespread practice for escaping the `/`, `?`, and `#` symbols in a path
|
||||
* Unicode symbols in domain names are allowed (though not universally supported) through a peculiar encoding technique called “[Punycode](https://www.rfc-editor.org/rfc/rfc3492.txt)”
|
||||
* Unicode symbols in domain names are allowed (though not universally supported) through a peculiar encoding technique called “Punycode[ref Punycode: A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)](https://www.rfc-editor.org/rfc/rfc3492.txt)”
|
||||
* Traditionally, different casings are used in different parts of an HTTP request:
|
||||
* `kebab-case`in domains, headers, and paths
|
||||
* `snake_case` in query parameters
|
||||
|
@ -47,7 +47,7 @@ This implies that a request traverses the following path:
|
||||
* C and D call Service A to check the authentication token (passed as a proxied `Authorization` header or as an explicit request parameter) and return the requested data — the user's profile and the list of their orders.
|
||||
* Service D merges the responses and sends them back to the client.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
|
||||
It is quite obvious that in this setup, we put excessive load on the authorization service as every nested microservice now needs to query it. Even if we abolish checking the authenticity of internal requests, it won't help as services B and C can't know the identifier of the user. Naturally, this leads to the idea of propagating the once-retrieved `user_id` through the microservice mesh:
|
||||
* Gateway D receives a request and exchanges the token for `user_id` through service A
|
||||
@ -60,7 +60,7 @@ It is quite obvious that in this setup, we put excessive load on the authorizati
|
||||
GET /v1/orders?user_id=<user id>
|
||||
```
|
||||
|
||||
[]()
|
||||
[]()
|
||||
|
||||
**NB**: We used the `/v1/orders?user_id` notation and not, let's say, `/v1/users/{user_id}/orders`, because of two reasons:
|
||||
* The orders service stores orders, not users, and it would be logical to reflect this fact in URLs
|
||||
@ -75,7 +75,7 @@ Let us emphasize that the difference between **stateless** and **stateful** appr
|
||||
* External entities should be just context identifiers, and microservices should not interpret them
|
||||
* If operations with external data are unavoidable (for example, the authority making a call must be checked), the **operations must be organized in a way that reduces them to checking the data integrity**.
|
||||
|
||||
In our example, we might get rid of unnecessary calls to service A in a different manner — by using stateless tokens, for example, employing the [JWT standard](https://www.rfc-editor.org/rfc/rfc7519). Then services B and C would be capable of deciphering tokens and extracting user identifiers on their own.
|
||||
In our example, we might get rid of unnecessary calls to service A in a different manner — by using stateless tokens, for example, employing the JWT standard[ref JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519). Then services B and C would be capable of deciphering tokens and extracting user identifiers on their own.
|
||||
|
||||
Let us take a step further and notice that the user profile rarely changes, so there is no need to retrieve it each time as we might cache it at the gateway level. To do so, we must form a cache key which is essentially the client identifier. We can do this by taking a long way:
|
||||
* Before requesting service B, generate a cache key and probe the cache
|
||||
@ -105,7 +105,7 @@ Then gateway D can be implemented following this scenario:
|
||||
* If service C responds with a `304 Not Modified` status code, return the cached state
|
||||
* If service C responds with a new version of the data, cache it and then return it to the client.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
|
||||
By employing this approach [using `ETag`s to control caching], we automatically get another pleasant bonus. We can reuse the same data in the order creation endpoint design. In the optimistic concurrency control paradigm, the client must pass the actual revision of the `orders` resource to change its state:
|
||||
|
||||
@ -133,7 +133,7 @@ ETag: <new revision>
|
||||
|
||||
and gateway D will update the cache with the current data snapshot.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
|
||||
**Importantly**, after this API refactoring, we end up with a system in which we can *remove gateway D* and make the client itself perform its duty. Nothing prevents the client from:
|
||||
* Storing `user_id` on its side (or retrieving it from the token, if the format allows it) as well as the last known `ETag` of the order list
|
||||
|
@ -41,7 +41,7 @@ This convention allows for reflecting almost any API's entity nomenclature decen
|
||||
|
||||
In other words, with any operation that runs an algorithm rather than returns a predefined result (such as listing offers relevant to a search phrase), we will have to decide what to choose: following verb semantics or indicating side effects? Caching the results or hinting that the results are generated on the fly?
|
||||
|
||||
**NB**: The authors of the standard are also concerned about this dichotomy and have finally [proposed the `QUERY` HTTP method](https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html), which is basically a safe (i.e., non-modifying) version of `POST`. However, we do not expect it to gain widespread adoption just as [the existing `SEARCH` verb](https://www.rfc-editor.org/rfc/rfc5323) did not.
|
||||
**NB**: The authors of the standard are also concerned about this dichotomy and have finally proposed the `QUERY` HTTP method[ref The HTTP QUERY Method](https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html), which is basically a safe (i.e., non-modifying) version of `POST`. However, we do not expect it to gain widespread adoption just as the existing `SEARCH` verb[ref Web Distributed Authoring and Versioning (WebDAV) SEARCH](https://www.rfc-editor.org/rfc/rfc5323) did not.
|
||||
|
||||
Unfortunately, we don't have simple answers to these questions. Within this book, we adhere to the following approach: the call signature should, first and foremost, be concise and readable. Complicating signatures for the sake of abstract concepts is undesirable. In relation to the mentioned issues, this means that:
|
||||
1. Operation metadata should not change the meaning of the operation. If a request reaches the final microservice without any headers at all, it should still be executable, although some auxiliary functionality may degrade or be absent.
|
||||
|
@ -61,7 +61,7 @@ Even if we choose this approach, there are very few status codes that can reflec
|
||||
|
||||
The editors of the specification are very well aware of this problem as they state that “the server SHOULD send a representation containing an explanation of the error situation, and whether it is a temporary or permanent condition.” This, however, contradicts the entire idea of a uniform machine-readable interface (and so does the idea of using arbitrary status codes). (Let us additionally emphasize that this lack of standard tools to describe business logic-bound errors is one of the reasons we consider the REST architectural style as described by Fielding in his 2008 article non-viable. The client *must* possess prior knowledge of error formats and how to work with them. Otherwise, it could restore its state after an error only by restarting the application.)
|
||||
|
||||
NB: Not long ago, the editors of the standard proposed their own version of the JSON description specification for HTTP errors — [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html). You can use it, but keep in mind that it covers only the most basic scenario:
|
||||
**NB**: Not long ago, the editors of the standard proposed their own version of the JSON description specification for HTTP errors — RFC 9457[ref RFC 9457 Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457.html). You can use it, but keep in mind that it covers only the most basic scenario:
|
||||
* The error subtype is not transmitted in the metadata.
|
||||
* There is no distinction between a message for the user and a message for the developer.
|
||||
* The specific machine-readable format for error descriptions is left to the discretion of the developer.
|
||||
|
@ -14,7 +14,7 @@ Additionally, we'd like to provide some code style advice:
|
||||
|
||||
2. Include common headers (such as `Date`, `Content-Type`, `Content-Encoding`, `Content-Length`, `Cache-Control`, `Retry-After`, etc.) in the responses and generally avoid relying on clients to guess default protocol parameters correctly.
|
||||
|
||||
3. Support the `OPTIONS` method and the [CORS protocol](https://fetch.spec.whatwg.org/#cors-protocol) just in case your API needs to be accessed from a Web browser.
|
||||
3. Support the `OPTIONS` method and the CORS protocol[ref Fetch Living Standard. CORS protocol](https://fetch.spec.whatwg.org/#http-cors-protocol) just in case your API needs to be accessed from a Web browser.
|
||||
|
||||
4. Choose a casing rule and a rule for transforming casing while moving a parameter from one part of an HTTP request to another.
|
||||
|
||||
@ -31,11 +31,11 @@ Additionally, we'd like to provide some code style advice:
|
||||
* Do not put parameters that require escaping (i.e., non-alphanumeric ones) in a path or a domain of a URL. Use query or body parameters for this purpose.
|
||||
|
||||
8. Familiarize yourself with at least the basics of typical vulnerabilities in HTTP APIs used by attackers, such as:
|
||||
* [CSRF](https://owasp.org/www-community/attacks/csrf)
|
||||
* [SSRF](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery)
|
||||
* [HTTP Response Splitting](https://owasp.org/www-community/attacks/HTTP_Response_Splitting)
|
||||
* [Unvalidated Redirects and Forwards](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html)
|
||||
* CSRF[ref Cross Site Request Forgery (CSRF)](https://owasp.org/www-community/attacks/csrf)
|
||||
* SSRF[ref Server Side Request Forgery](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery)
|
||||
* HTTP Response Splitting[ref HTTP Response Splitting](https://owasp.org/www-community/attacks/HTTP_Response_Splitting)
|
||||
* Unvalidated Redirects and Forwards[ref Unvalidated Redirects and Forwards Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html)
|
||||
|
||||
and include protection against these attack vectors at the webserver software level. The OWASP community provides a [good cheatsheet on the best HTTP API security practices](https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html).
|
||||
and include protection against these attack vectors at the webserver software level. The OWASP community provides a good cheatsheet on the best HTTP API security practices.[ref REST Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html)
|
||||
|
||||
In conclusion, we would like to make the following statement: building an HTTP API is relying on the common knowledge of HTTP call semantics and drawing benefits from it by leveraging various software built upon this paradigm, from client frameworks to server gateways, and developers reading and understanding API specifications. In this sense, the HTTP ecosystem provides probably the most comprehensive vocabulary, both in terms of profoundness and adoption, compared to other technologies, allowing for describing many different situations that may arise in client-server communication. While the technology is not perfect and has its flaws, for a *public API* vendor, it is the default choice, and opting for other technologies rather needs to be substantiated as of today.
|
@ -1 +1,232 @@
|
||||
### Decomposing UI Components
|
||||
### [Decomposing UI Components][sdk-decomposing]
|
||||
|
||||
Let's transit to a more substantive conversation and try to understand, why the requirement to allow replacing component's subsystems with alternative implementations leads to dramatic interface inflation. We continue studying the `SearchBox` component from the previous chapter. Let us remind the reader the factors that complicate designing APIs for visual components:
|
||||
* Coupling heterogeneous functionality (such as business logic, appearance styling, and behavior) in one entity
|
||||
* Introducing shared resources, i.e. an object state that could be simultaneously modified by different actors (including the end-user)
|
||||
* Emerging of ambivalent hierarchies of inheritance of entity's properties and options.
|
||||
|
||||
We will make a task more specific. Imagine we need to develop a `SearchBox` that allows for the following modifications:
|
||||
1. Replacing the textual paragraphs representing an offer with a map with markers that could be highlighted:
|
||||
* Illustrates the problem of replacing a subcomponent (the offer list) while preserving behavior and design of other parts of the system; also, the complexity of implementing shared states.
|
||||
|
||||
[]()
|
||||
|
||||
2. Combining short and full descriptions of an offer in a single UI (a list item could be expanded, and the order can be created in-place):
|
||||
* Illustrates the problem of fully removing a subcomponent and transferring its business logic to other parts of the system.
|
||||
|
||||
[]()
|
||||
|
||||
[]()
|
||||
|
||||
3. Manipulating the data presented to the user and the available actions for an offer through adding new buttons, such as “Previous offer,” “Next offer,” and “Make a call.”
|
||||
|
||||
[]()
|
||||
|
||||
In this scenario, we're evaluating different chains of propagating data and options down to the offer panel and building dynamic UIs on top of it:
|
||||
|
||||
* Some data fields (such as logo and phone number) are properties of a real object received in the search API response.
|
||||
|
||||
* Some data fields make sense only in the context of this specific UI and reflect its design principles (for instance, the “Previous” and “Next” buttons).
|
||||
|
||||
* Some data fields (such as the icons of the “Not now” and “Make a call” buttons) are bound to the button type (i.e., the business logic it provides).
|
||||
|
||||
The obvious approach to tackle these scenarios appears to be creating two additional subcomponents responsible for presenting a list of offers and the details of the specific offer. Let's name them `OfferList` and `OfferPanel` respectively.
|
||||
|
||||
[]()
|
||||
|
||||
If we had no customization requirements, the pseudo-code implementing interactions between all three components would look rather trivial:
|
||||
|
||||
```typescript
|
||||
class SearchBox implements ISearchBox {
|
||||
// The responsibility of `SearchBox` is:
|
||||
// 1. Creating a container for rendering
|
||||
// an offer list, prepare option values
|
||||
// and create the `OfferList` instance
|
||||
constructor(container, options) {
|
||||
…
|
||||
this.offerList = new OfferList(
|
||||
this,
|
||||
offerListContainer,
|
||||
offerListOptions
|
||||
);
|
||||
}
|
||||
// 2. Making an offer search when a user
|
||||
// presses the corresponding button and
|
||||
// to provide analogous programmable
|
||||
// interface for developers
|
||||
onSearchButtonClick() {
|
||||
this.search(this.searchInput.value);
|
||||
}
|
||||
search(query) {
|
||||
…
|
||||
}
|
||||
// 3. Notifying about new search results
|
||||
// being received from the server
|
||||
onSearchResultsReceived(searchResults) {
|
||||
…
|
||||
this.offerList.setOfferList(searchResults)
|
||||
}
|
||||
// 4. Creating orders (and manipulate sub-
|
||||
// components if needed)
|
||||
createOrder(offer) {
|
||||
this.offerListDestroy();
|
||||
ourCoffeeSdk.createOrder(offer);
|
||||
…
|
||||
}
|
||||
// 5. Self-destructing when requested to
|
||||
destroy() {
|
||||
this.offerList.destroy();
|
||||
…
|
||||
}
|
||||
}
|
||||
```
|
||||
```typescript
|
||||
class OfferList implements IOfferList {
|
||||
// The responsibility of `OfferList` is:
|
||||
// 1. Creating a container for rendering
|
||||
// an offer panel, prepare option values
|
||||
// and create the `OfferPanel` instance
|
||||
constructor(searchBox, container, options) {
|
||||
…
|
||||
this.offerPanel = new OfferPanel(
|
||||
searchBox,
|
||||
offerPanelContainer,
|
||||
offerPanelOptions
|
||||
);
|
||||
…
|
||||
}
|
||||
// 2. Providing a method to change the list
|
||||
// of offers to be presented
|
||||
setOfferList(offerList) { … }
|
||||
// 3. When an offer is selected, opening
|
||||
// an offer panel to present it
|
||||
onOfferClick(offer) {
|
||||
this.offerPanel.show(offer)
|
||||
}
|
||||
// 4. Self-destructing if requested to
|
||||
destroy() {
|
||||
this.offerPanel.destroy();
|
||||
…
|
||||
}
|
||||
}
|
||||
```
|
||||
```typescript
|
||||
class OfferPanel implements IOfferPanel {
|
||||
constructor(
|
||||
searchBox, container, options
|
||||
) { … }
|
||||
// The responsibility of `OfferPanel` is:
|
||||
// 1. Presenting an offer
|
||||
show(offer) {
|
||||
this.offer = offer;
|
||||
…
|
||||
}
|
||||
// 2. Creating an order when the user
|
||||
// presses the “Place an order” button
|
||||
onCreateOrderButtonClick() {
|
||||
this.searchBox.createOrder(this.offer);
|
||||
}
|
||||
// 3. Closing itself when the user
|
||||
// presses the “Not now” button
|
||||
onCancelButtonClick() {
|
||||
// …
|
||||
}
|
||||
// 4. Self-destructing if requested to
|
||||
destroy() { … }
|
||||
}
|
||||
```
|
||||
|
||||
The `ISearchBox` / `IOfferPanel` / `IOfferView` interfaces are concise as well (constructors and destructors omitted):
|
||||
|
||||
```typescript
|
||||
interface ISearchBox {
|
||||
search(query);
|
||||
createOrder(offer);
|
||||
}
|
||||
interface IOfferList {
|
||||
setOfferList(offerList);
|
||||
}
|
||||
interface IOfferPanel {
|
||||
show(offer);
|
||||
}
|
||||
```
|
||||
|
||||
If we aren't making an SDK and have not had the task of making these components customizable, the approach would be perfectly viable. However, let's discuss how would we solve the three sample tasks described above.
|
||||
|
||||
1. Displaying an offer list on the map: at first glance, we can develop an alternative component for displaying offers that implement the `IOfferList` interface (let's call it `OfferMap`) that will reuse the standard offer panel. However, we have a problem: `OfferList` only sends commands to `OfferPanel` while `OfferMap` also needs to receive feedback: an event of panel closure to deselect a marker. API of our components does not encompass this functionality, and implementing it is not that simple:
|
||||
|
||||
```typescript
|
||||
class CustomOfferPanel extends OfferPanel {
|
||||
constructor(
|
||||
searchBox, offerMap, container, options
|
||||
) {
|
||||
this.offerMap = offerMap;
|
||||
super(searchBox, container, options);
|
||||
}
|
||||
onCancelButtonClick() {
|
||||
/* <em> */offerMap.resetCurrentOffer();/* </em> */
|
||||
super.onCancelButtonClick();
|
||||
}
|
||||
}
|
||||
class OfferMap implements IOfferList {
|
||||
constructor(searchBox, container, options) {
|
||||
…
|
||||
this.offerPanel = new CustomOfferPanel(
|
||||
this,
|
||||
searchBox,
|
||||
offerPanelContainer,
|
||||
offerPanelOptions
|
||||
)
|
||||
}
|
||||
resetCurrentOffer() { … }
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
We have to create a `CustomOfferPanel` class, and this implementation, unlike its parent class, now only works with `OfferMap`, not with any `IOfferList`-compatible component.
|
||||
|
||||
2. The case of making full offer details and action controls in place in the offer list is pretty obvious: we can achieve this only by writing a new `IOfferList`-compatible component from scratch as whatever overrides we apply to standard `OfferList`, it will continue creating an `OfferPanel` and open it upon offer selection.
|
||||
|
||||
3. To implement new buttons, we can only propose developers creating a custom offer list component (to provide methods for selecting previous and next offers) and a custom offer panel that will call these methods. If we find a simple solution for customizing, let's say, the “Place an order” button text, this solution needs to be supported in the `OfferList` code:
|
||||
|
||||
```typescript
|
||||
const searchBox = new SearchBox(…, {
|
||||
/* <em> */offerPanelCreateOrderButtonText:
|
||||
'Drink overpriced coffee!'/* </em> */
|
||||
});
|
||||
|
||||
class OfferList {
|
||||
constructor(…, options) {
|
||||
…
|
||||
// It is `OfferList`'s responsibility
|
||||
// to isolate the injection point and
|
||||
// to propagate the overriden value
|
||||
// to the `OfferPanel` instance
|
||||
this.offerPanel = new OfferPanel(…, {
|
||||
/* <em> */createOrderButtonText: options
|
||||
.offerPanelCreateOrderButtonText/* </em> */
|
||||
…
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The solutions we discuss are also poorly extendable. For example, in \#1, if we decide to make the offer list reaction to closing an offer panel a part of a standard interface for developers to use it, we will need to add a new method to the `IOfferList` interface and make it optional to maintain backward compatibility:
|
||||
|
||||
```typescript
|
||||
interface IOfferList {
|
||||
…
|
||||
onOfferPanelClose?();
|
||||
}
|
||||
```
|
||||
|
||||
In the `OfferPanel` code, the support of this new method will look like:
|
||||
|
||||
```typescript
|
||||
if (Type(this.offerList.onOfferPanelClose)
|
||||
== 'function') {
|
||||
this.offerList.onOfferPanelClose();
|
||||
}
|
||||
```
|
||||
|
||||
For sure, this will not make our code any nicer. Additionally, `OfferList` and `OfferPanel` will become even more tightly coupled.
|
@ -21,7 +21,7 @@ However, it often makes sense to provide multiple API services that feature the
|
||||
2. Next is the basic level, which involves working with product entities through formal interfaces. [In our study example, this level corresponds to the HTTP API for placing orders.]
|
||||
3. Working with product entities can be simplified by providing SDKs for popular platforms that tailor API concepts according to the paradigms of those platforms. This benefits developers who are proficient with specific platforms and saves them effort in dealing with formal protocols and interfaces.
|
||||
4. The next simplification step is providing services for code generation. In this service, developers can choose from pre-built integration templates, customize option values, and obtain a ready-to-use piece of code that can be easily copied and pasted into their application code (and can be further 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 developers to place UI elements and generate the working application code or a console script to automatically produce application boilerplate.]
|
||||
5. Finally, this approach can be simplified even further if the service generates not just code but a ready-to-use component / widget / frame and a one-liner to integrate it. [For example, if we allow embedding an iframe that handles the entire coffee ordering process directly on the partner's website, or describe the rules of forming a “[deep link](https://en.wikipedia.org/wiki/Mobile_deep_linking)” to our service.]
|
||||
5. Finally, this approach can be simplified even further if the service generates not just code but a ready-to-use component / widget / frame and a one-liner to integrate it. [For example, if we allow embedding an iframe that handles the entire coffee ordering process directly on the partner's website, or describe the rules of forming a “deep link[ref Mobile Deep Linking](https://en.wikipedia.org/wiki/Mobile_deep_linking)” to our service.]
|
||||
|
||||
Ultimately, we will end up with the concept of a meta-API where these high-level components have their own API built on top of the basic API.
|
||||
|
||||
|
@ -8,6 +8,8 @@
|
||||
"copyright": "© Sergey Konstantinov, 2023",
|
||||
"locale": "en_US",
|
||||
"file": "API",
|
||||
"isbn": "ISBN",
|
||||
"references": "References",
|
||||
"aboutMe": {
|
||||
"title": "About the Author",
|
||||
"content": [
|
||||
@ -15,9 +17,8 @@
|
||||
"<p>During this tenure, Sergey gained unique experience in building world-class APIs with a daily audience of tens of millions, planning roadmaps for such a service, and delivering numerous public speeches. Additionaly, he served as a member of the W3C Technical Architecture Group for a year and a half.</p>",
|
||||
"<p>After being nine years in Maps, Sergey transitioned to technical lead roles in other departments and companies. In these positions, he led integration efforts and was responsible for the technical architecture of entire business units. Currently, Sergey resides in Tallinn, Estonia, and works as a staff software engineer at Bolt.</p>"
|
||||
],
|
||||
"imageCredit": "Photo by <a href=\"http://linkedin.com/in/zloylos/\">Denis Hananein</a>"
|
||||
"imageCredit": "Photo by <a href=\"https://linkedin.com/in/zloylos/\">Denis Hananein</a>"
|
||||
},
|
||||
"ctl": "Click to enlarge",
|
||||
"landingFile": "index.html",
|
||||
"url": "https://twirl.github.io/The-API-Book/",
|
||||
"favicon": "/img/favicon.png",
|
||||
@ -45,7 +46,7 @@
|
||||
},
|
||||
{
|
||||
"key": "reddit",
|
||||
"link": "http://www.reddit.com/submit?url=${url}&title=${text}"
|
||||
"link": "https://www.reddit.com/submit?url=${url}&title=${text}"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -84,7 +85,7 @@
|
||||
"<li>API product management.</ul>",
|
||||
"<p class=\"text-align-left\">Illustrations & inspiration by Maria Konstantinova · <a href=\"https://www.instagram.com/art.mari.ka/\">art.mari.ka</a></p>",
|
||||
"<img class=\"cc-by-nc-img\" alt=\"Creative Commons «Attribution-NonCommercial» Logo\" src=\"https://i.creativecommons.org/l/by-nc/4.0/88x31.png\"/>",
|
||||
"<p class=\"cc-by-nc\">This book is distributed under the <a href=\"http://creativecommons.org/licenses/by-nc/4.0/\">Creative Commons Attribution-NonCommercial 4.0 International licence</a>.</p>"
|
||||
"<p class=\"cc-by-nc\">This book is distributed under the <a href=\"https://creativecommons.org/licenses/by-nc/4.0/\">Creative Commons Attribution-NonCommercial 4.0 International licence</a>.</p>"
|
||||
]
|
||||
},
|
||||
"landing": {
|
||||
@ -110,7 +111,7 @@
|
||||
"or": "or",
|
||||
"readOnline": "read it online",
|
||||
"liveExamples": "Live Examples",
|
||||
"license": "This book is distributed under the <a href=\"http://creativecommons.org/licenses/by-nc/4.0/\">Creative Commons Attribution-NonCommercial 4.0 International licence</a>.",
|
||||
"license": "This book is distributed under the <a href=\"https://creativecommons.org/licenses/by-nc/4.0/\">Creative Commons Attribution-NonCommercial 4.0 International licence</a>.",
|
||||
"footer": [
|
||||
"<p>Книгу «API» можно <a href=\"index.ru.html\">читать по-русски</a>.</p>"
|
||||
]
|
||||
|
Binary file not shown.
BIN
src/fonts/RobotoMono-VariableFont_wght.ttf
Normal file
BIN
src/fonts/RobotoMono-VariableFont_wght.ttf
Normal file
Binary file not shown.
BIN
src/img/mockups/09.png
Normal file
BIN
src/img/mockups/09.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 105 KiB |
@ -19,9 +19,9 @@
|
||||
К первой категории относятся API, которые принято обозначать словом «REST» или «RESTful». Вторая категория, в основном, представлена разными видами RPC-протоколов.
|
||||
|
||||
**Во-вторых**, реализации HTTP API опираются на разные форматы передаваемых данных:
|
||||
* REST API и некоторые RPC ([JSON-RPC](https://www.jsonrpc.org/), [GraphQL](https://graphql.org/)) полагаются в основном на формат [JSON](https://www.ecma-international.org/publications-and-standards/standards/ecma-404/) (опционально дополненный передачей бинарных файлов);
|
||||
* [gRPC](https://grpc.io/), а также [Apache Avro](https://avro.apache.org/docs/) и другие специализированные RPC-протоколы полагаются на бинарные форматы (такие как [Protocol Buffers](https://protobuf.dev/), [FlatBuffers](https://flatbuffers.dev/) и собственный формат Apache Avro);
|
||||
* наконец, некоторые RPC-протоколы ([SOAP](https://www.w3.org/TR/soap12/), [XML-RPC](http://xmlrpc.com/)) используют для передачи данных формат [XML](https://www.w3.org/TR/xml/) (что многими разработчиками сегодня воспринимается скорее как устаревшая практика).
|
||||
* REST API и некоторые RPC (JSON-RPC[ref JSON-RPC](https://www.jsonrpc.org/), GraphQL[ref GraphQL](https://graphql.org/)) полагаются в основном на формат JSON[ref JSON](https://www.ecma-international.org/publications-and-standards/standards/ecma-404/) (опционально дополненный передачей бинарных файлов);
|
||||
* gRPC[ref gRPC](https://grpc.io/), а также Apache Avro[ref Apache Avro](https://avro.apache.org/docs/) и другие специализированные RPC-протоколы полагаются на бинарные форматы (такие как Protocol Buffers[ref Protocol Buffers](https://protobuf.dev/), FlatBuffers[ref FlatBuffers](https://flatbuffers.dev/) и собственный формат Apache Avro);
|
||||
* наконец, некоторые RPC-протоколы (SOAP[ref SOAP](https://www.w3.org/TR/soap12/), XML-RPC[ref XML-RPC](http://xmlrpc.com/)) используют для передачи данных формат XML[ref Extensible Markup Language (XML)](https://www.w3.org/TR/xml/) (что многими разработчиками сегодня воспринимается скорее как устаревшая практика).
|
||||
|
||||
Все перечисленные технологии оперируют существенно разными парадигмами — и вызывают естественным образом большое количество холиваров — хотя на момент написания этой книги можно констатировать, что для API общего назначения выбор практически сводится к триаде «REST API (фактически, JSON over HTTP) против gRPC против GraphQL».
|
||||
|
||||
@ -31,6 +31,6 @@ HTTP API будет посвящён раздел IV; мы также отдел
|
||||
|
||||
Понятие SDK (Software Development Kit, «набор для разработки программного обеспечения»), вообще говоря, вовсе не относится к API: это просто термин для некоторого набора программных инструментов. Однако, как и за «REST», за ним закрепилось некоторое определённое толкование — как клиентского фреймворка для работы с некоторым API. Это может быть как обёртка над клиент-серверным API, так и UI-библиотека в рамках какой-то платформы. Существенным отличием от вышеперечисленных API является то, что «SDK» реализован для какого-то конкретного языка программирования и предоставляет возможность работать с низкоуровневым API нижележащей платформы.
|
||||
|
||||
В отличие от клиент-серверных API, обобщить такие SDK не представляется возможным, т.к. каждый из них написан под конкретное сочетание язык программирования-платформа. Из интероперабельных технологий в мире SDK можно привести в пример кросс-платформенные мобильные ([React Native](https://reactnative.dev/), [Flutter](https://flutter.dev/), [Xamarin](https://dotnet.microsoft.com/en-us/apps/xamarin)) и десктопные фреймворки ([JavaFX](https://openjfx.io/), QT) и некоторые узкоспециализированные решения ([Unity](https://docs.unity3d.com/Manual/index.html)), однако все они направлены на работу с конкретными технологиями и весьма специфичны.
|
||||
В отличие от клиент-серверных API, обобщить такие SDK не представляется возможным, т.к. каждый из них написан под конкретное сочетание язык программирования-платформа. Из интероперабельных технологий в мире SDK можно привести в пример кросс-платформенные мобильные (React Native[ref React Native](https://reactnative.dev/), Flutter[ref Flutter](https://flutter.dev/), Xamarin[ref Xamarin](https://dotnet.microsoft.com/en-us/apps/xamarin)) и десктопные фреймворки (JavaFX[ref JavaFX](https://openjfx.io/), QT[ref QT](https://www.qt.io/)) и некоторые узкоспециализированные решения (Unity[ref Unity](https://docs.unity3d.com/Manual/index.html)), однако все они направлены на работу с конкретными технологиями и весьма специфичны.
|
||||
|
||||
Тем не менее, SDK обладают общностью *на уровне задач*, которые они решают, и именно этому (решению проблем трансляции и предоставления UI-компонент) будет посвящён раздел V настоящей книги.
|
@ -1,6 +1,6 @@
|
||||
### [О версионировании][intro-versioning]
|
||||
|
||||
Здесь и далее мы будем придерживаться принципов версионирования [semver](https://semver.org/).
|
||||
Здесь и далее мы будем придерживаться принципов версионирования Semantic Versioning (semver)[ref Semantic Versioning](https://semver.org/).
|
||||
|
||||
1. Версия API задаётся тремя цифрами вида `1.2.3`.
|
||||
2. Первая цифра (мажорная версия) увеличивается при обратно несовместимых изменениях в API.
|
||||
|
@ -209,7 +209,7 @@ POST /v1/orders
|
||||
|
||||
Исходя из нашего собственного опыта использования разных API, мы можем, не колеблясь, сказать, что самая большая ошибка проектирования сущностей в API (и, соответственно, головная боль разработчиков) — чрезмерная перегруженность интерфейсов полями, методами, событиями, параметрами и прочими атрибутами сущностей.
|
||||
|
||||
При этом существует «золотое правило», применимое не только к API, но ко множеству других областей проектирования: человек комфортно удерживает в краткосрочной памяти 7±2 различных объекта. Манипулировать большим числом сущностей человеку уже сложно. Это правило также известно как [«закон Миллера»](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B1%D0%BE%D1%87%D0%B0%D1%8F_%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D1%8C#%D0%9E%D1%86%D0%B5%D0%BD%D0%BA%D0%B0_%D0%B5%D0%BC%D0%BA%D0%BE%D1%81%D1%82%D0%B8_%D1%80%D0%B0%D0%B1%D0%BE%D1%87%D0%B5%D0%B9_%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D0%B8).
|
||||
При этом существует «золотое правило», применимое не только к API, но ко множеству других областей проектирования: человек комфортно удерживает в краткосрочной памяти 7±2 различных объекта. Манипулировать большим числом сущностей человеку уже сложно. Это правило также известно как «закон Миллера»[ref Рабочая память. Оценка емкости рабочей памяти](https://ru.wikipedia.org/wiki/Рабочая_память#Оценка_емкости_рабочей_памяти).
|
||||
|
||||
Бороться с этим законом можно только одним способом: декомпозицией. На каждом уровне работы с вашим API нужно стремиться логически группировать сущности под одним именем там, где это возможно и таким образом, чтобы разработчику никогда не приходилось оперировать более чем 10 сущностями одновременно.
|
||||
|
||||
|
@ -193,7 +193,7 @@ str_replace(needle, replace, haystack)
|
||||
**Лучше**: `"prohibit_calling": true` или `"avoid_calling": true`
|
||||
— читается лучше, хотя обольщаться всё равно не следует. Насколько это возможно откажитесь от семантически двойных отрицаний, даже если вы придумали «негативное» слово без явной приставки «не».
|
||||
|
||||
Стоит также отметить, что в использовании [законов де Моргана](https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BA%D0%BE%D0%BD%D1%8B_%D0%B4%D0%B5_%D0%9C%D0%BE%D1%80%D0%B3%D0%B0%D0%BD%D0%B0) ошибиться ещё проще, чем в двойных отрицаниях. Предположим, что у вас есть два флага:
|
||||
Стоит также отметить, что в использовании законов де Моргана[ref Законы де Моргана](https://ru.wikipedia.org/wiki/Законы_де_Моргана) ошибиться ещё проще, чем в двойных отрицаниях. Предположим, что у вас есть два флага:
|
||||
|
||||
```
|
||||
GET /coffee-machines/{id}/stocks
|
||||
@ -528,7 +528,7 @@ POST /v1/coffee-machines/search
|
||||
}
|
||||
```
|
||||
|
||||
Формально, подобное умолчание допустимо — почему бы не иметь концепции «географических координат по умолчанию». Однако в реальности результатом подобных политик «тихого» исправления ошибок становятся абсурдные ситуации типа «null island» — [самой посещаемой точки в мире](https://www.sciencealert.com/welcome-to-null-island-the-most-visited-place-that-doesn-t-exist). Чем популярнее API, тем больше шансов, что партнеры просто не обратят внимания на такие пограничные ситуации.
|
||||
Формально, подобное умолчание допустимо — почему бы не иметь концепции «географических координат по умолчанию». Однако в реальности результатом подобных политик «тихого» исправления ошибок становятся абсурдные ситуации типа «null island» — самой посещаемой точки в мире[ref Hrala, J. Welcome to Null Island, The Most 'Visited' Place on Earth That Doesn't Actually Exist](https://www.sciencealert.com/welcome-to-null-island-the-most-visited-place-that-doesn-t-exist). Чем популярнее API, тем больше шансов, что партнеры просто не обратят внимания на такие пограничные ситуации.
|
||||
|
||||
**Хорошо**:
|
||||
```
|
||||
@ -868,11 +868,11 @@ X-Idempotency-Token: <токен>
|
||||
|
||||
Если бы автору этой книги давали доллар каждый раз, когда ему приходилось бы имплементировать кем-то придуманный дополнительный протокол безопасности — он бы давно уже был на заслуженной пенсии. Любовь разработчиков API к подписыванию параметров запросов или сложным схемам обмена паролей на токены столь же несомненна, сколько и бессмысленна.
|
||||
|
||||
**Во-первых**, почти всегда процедуры, обеспечивающие безопасность той или иной операции, *уже разработаны*. Нет никакой нужды придумывать их заново, просто имплементируйте какой-то из существующих протоколов. Никакие самописные алгоритмы проверки сигнатур запросов не обеспечат вам того же уровня защиты от атаки [Man-in-the-Middle](https://en.wikipedia.org/wiki/Man-in-the-middle_attack), как соединение по протоколу TLS с взаимной проверкой сигнатур сертификатов.
|
||||
**Во-первых**, почти всегда процедуры, обеспечивающие безопасность той или иной операции, *уже разработаны*. Нет никакой нужды придумывать их заново, просто имплементируйте какой-то из существующих протоколов. Никакие самописные алгоритмы проверки сигнатур запросов не обеспечат вам того же уровня защиты от атаки Manipulator-in-the-middle (*MitM*)[ref Manipulator-in-the-middle Attack](https://owasp.org/www-community/attacks/Manipulator-in-the-middle_attack), как соединение по протоколу TLS с взаимной проверкой сигнатур сертификатов[ref Mutual Authentication. mTLS](https://en.wikipedia.org/wiki/Mutual_authentication#mTLS).
|
||||
|
||||
**Во-вторых**, чрезвычайно самонадеянно (и опасно) считать, что вы разбираетесь в вопросах безопасности. Новые вектора атаки появляются каждый день, и быть в курсе всех актуальных проблем — это само по себе работа на полный рабочий день. Если же вы полный рабочий день занимаетесь чем-то другим, спроектированная вами система защиты наверняка будет содержать уязвимости, о которых вы просто никогда не слышали — например, ваш алгоритм проверки паролей может быть подвержен [атаке по времени](https://en.wikipedia.org/wiki/Timing_attack), а веб-сервер — [атаке с разделением запросов](https://capec.mitre.org/data/definitions/105.html).
|
||||
**Во-вторых**, чрезвычайно самонадеянно (и опасно) считать, что вы разбираетесь в вопросах безопасности. Новые вектора атаки появляются каждый день, и быть в курсе всех актуальных проблем — это само по себе работа на полный рабочий день. Если же вы полный рабочий день занимаетесь чем-то другим, спроектированная вами система защиты наверняка будет содержать уязвимости, о которых вы просто никогда не слышали — например, ваш алгоритм проверки паролей может быть подвержен атаке по времени[ref Timing Attack](https://en.wikipedia.org/wiki/Timing_attack), а веб-сервер — атаке с разделением запросов[ref HTTP Request Splitting](https://capec.mitre.org/data/definitions/105.html).
|
||||
|
||||
Фонд OWASP каждый год [составляет список самых распространённых уязвимостей в API](https://owasp.org/www-project-api-security/), который мы настоятельно рекомендуем изучить.
|
||||
Фонд OWASP каждый год составляет список самых распространённых уязвимостей в API[ref OWASP API Security Project](https://owasp.org/www-project-api-security/), который мы настоятельно рекомендуем изучить.
|
||||
|
||||
Отдельно уточним: любые API должны предоставляться строго по протоколу TLS версии не ниже 1.2 (лучше 1.3).
|
||||
|
||||
@ -951,7 +951,7 @@ POST /v1/run/sql
|
||||
|
||||
##### Используйте глобально уникальные идентификаторы
|
||||
|
||||
Хорошим тоном при разработке API будет использование для идентификаторов сущностей глобально уникальных строк, либо семантичных (например, "lungo" для видов напитков), либо случайных (например [UUID-4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random))). Это может чрезвычайно пригодиться, если вдруг придётся объединять данные из нескольких источников под одним идентификатором.
|
||||
Хорошим тоном при разработке API будет использование для идентификаторов сущностей глобально уникальных строк, либо семантичных (например, "lungo" для видов напитков), либо случайных (например UUID-4[ref UUID-4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random))). Это может чрезвычайно пригодиться, если вдруг придётся объединять данные из нескольких источников под одним идентификатором.
|
||||
|
||||
Мы вообще склонны порекомендовать использование идентификаторов в urn-подобном формате, т.е. `urn:order:<uuid>` (или просто `order:<uuid>`), это сильно помогает с отладкой legacy-систем, где по историческим причинам есть несколько разных идентификаторов для одной и той же сущности, в таком случае неймспейсы в urn помогут быстро понять, что это за идентификатор и нет ли здесь ошибки использования.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
### О паттернах проектирования в контексте API
|
||||
|
||||
Концепция [«паттернов»](https://en.wikipedia.org/wiki/Software_design_pattern#History) в области разработки программного обеспечения была введена Кентом Бэком и Уордом Каннингемом в 1987 году, и популяризирован «бандой четырёх» (Эрих Гамма, Ричард Хелм, Ральф Джонсон и Джон Влиссидес) в их книге «Приёмы объектно-ориентированного проектирования. Паттерны проектирования», изданной в 1994 году. Согласно общепринятому определению, паттерны программирования — «повторяемая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста».
|
||||
Концепция «паттернов» в области разработки программного обеспечения была введена Кентом Бэком и Уордом Каннингемом в 1987 году[ref Software Design Pattern. History](https://en.wikipedia.org/wiki/Software_design_pattern#History), и популяризирован «бандой четырёх» (Эрих Гамма, Ричард Хелм, Ральф Джонсон и Джон Влиссидес) в их книге «Приёмы объектно-ориентированного проектирования. Паттерны проектирования», изданной в 1994 году[ref:{"short":"Gamma, E., Helm, R., Johnson, R., Vlissides, J. (1994)","extra":["Design Patterns","Elements of Reusable Object-Oriented Software"]}](isbn:9780321700698). Согласно общепринятому определению, паттерны программирования — «повторяемая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста».
|
||||
|
||||
Если мы говорим об API, особенно если конечным потребителем этих API является разработчик (интерфейсы фреймворков, операционных систем), классические паттерны проектирования вполне к ним применимы. И действительно, многие из описанных в предыдущем разделе примеров представляют собой применение того или иного паттерна.
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
4. Сервер, наконец, получает запрос на создание заказа и исполняет его.
|
||||
5. Клиент, не зная об этом, создаёт заказ повторно.
|
||||
|
||||
Поскольку действия чтения списка актуальных заказов и создания нового заказа разнесены во времени, мы не можем гарантировать, что между этими запросами состояние системы не изменилось. Если же мы хотим такую гарантию дать, нам нужно обеспечить какую-то из [стратегий синхронизации](https://en.wikipedia.org/wiki/Synchronization_(computer_science)). Если в случае, скажем, API операционных систем или клиентских фреймворков мы можем воспользоваться предоставляемыми платформой примитивами, то в кейсе распределённых сетевых API такой примитив нам придётся разработать самостоятельно.
|
||||
Поскольку действия чтения списка актуальных заказов и создания нового заказа разнесены во времени, мы не можем гарантировать, что между этими запросами состояние системы не изменилось. Если же мы хотим такую гарантию дать, нам нужно обеспечить какую-то из стратегий синхронизации[ref Synchronization (Computer Science)](https://en.wikipedia.org/wiki/Synchronization_(computer_science)). Если в случае, скажем, API операционных систем или клиентских фреймворков мы можем воспользоваться предоставляемыми платформой примитивами, то в кейсе распределённых сетевых API такой примитив нам придётся разработать самостоятельно.
|
||||
|
||||
Существуют два основных подхода к решению этой проблемы — пессимистичный (программная реализация блокировок) и оптимистичный (версионирование ресурсов).
|
||||
|
||||
@ -58,7 +58,7 @@ try {
|
||||
|
||||
#### Оптимистичное управление параллелизмом
|
||||
|
||||
Более щадящий с точки зрения сложности имплементации вариант — это реализовать [оптимистичное управление параллелизмом](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) и потребовать от клиента передавать признак того, что он располагает актуальным состоянием разделяемого ресурса.
|
||||
Более щадящий с точки зрения сложности имплементации вариант — это реализовать оптимистичное управление параллелизмом[ref Optimistic Concurrency Control](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) и потребовать от клиента передавать признак того, что он располагает актуальным состоянием разделяемого ресурса.
|
||||
|
||||
```
|
||||
// Получаем состояние
|
||||
@ -84,7 +84,7 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
**NB**: внимательный читатель может возразить нам, что необходимость имплементировать стратегии синхронизации и строгую консистентность никуда не пропала, т.к. где-то в системе должен существовать компонент, осуществляющий блокирующее чтение версии с её последующим изменением. Это не совсем так: стратегии синхронизации и строгая консистентность *пропали из публичного API*. Расстояние между клиентом, устанавливающим блокировку, и сервером, её обрабатывающим, стало намного меньше, и всё взаимодействие теперь происходит в контролируемой среде (это вообще может быть одна подсистема, если мы используем [ACID-совместимую базу данных](https://en.wikipedia.org/wiki/ACID) или вовсе держим состояние ресурса в оперативной памяти).
|
||||
**NB**: внимательный читатель может возразить нам, что необходимость имплементировать стратегии синхронизации и строгую консистентность никуда не пропала, т.к. где-то в системе должен существовать компонент, осуществляющий блокирующее чтение версии с её последующим изменением. Это не совсем так: стратегии синхронизации и строгая консистентность *пропали из публичного API*. Расстояние между клиентом, устанавливающим блокировку, и сервером, её обрабатывающим, стало намного меньше, и всё взаимодействие теперь происходит в контролируемой среде (это вообще может быть одна подсистема, если мы используем ACID-совместимую базу данных[ref ACID](https://en.wikipedia.org/wiki/ACID) или вовсе держим состояние ресурса в оперативной памяти).
|
||||
|
||||
Вместо версий можно использовать дату последней модификации ресурса (что в целом гораздо менее надёжно ввиду неидеальной синхронизации часов в разных узлах системы; не забывайте, как минимум, сохранять дату с максимально доступной точностью!) либо идентификаторы сущности (ETag).
|
||||
|
||||
|
@ -21,7 +21,7 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
Т.к. заказы создаются намного реже, нежели читаются, мы можем существенно повысить производительность системы, если откажемся от гарантии возврата всегда самого актуального состояния ресурса из операции на чтение. Версионирование же поможет нам избежать проблем: создать заказ, не получив актуальной версии, невозможно. Фактически мы пришли к модели [событийной консистентности](https://en.wikipedia.org/wiki/Consistency_model#Eventual_consistency) (т.н. «согласованность в конечном счёте»): клиент сможет выполнить свой запрос *когда-нибудь*, когда получит, наконец, актуальные данные. В самом деле, согласованность в конечном счёте — скорее норма жизни для современных микросервисных архитектур, в которой может оказаться очень сложно как раз добиться обратного, т.е. строгой консистентности.
|
||||
Т.к. заказы создаются намного реже, нежели читаются, мы можем существенно повысить производительность системы, если откажемся от гарантии возврата всегда самого актуального состояния ресурса из операции на чтение. Версионирование же поможет нам избежать проблем: создать заказ, не получив актуальной версии, невозможно. Фактически мы пришли к модели событийной консистентности[ref Consistency Model. Eventual Consistency](https://en.wikipedia.org/wiki/Consistency_model#Eventual_consistency) (т.н. «согласованность в конечном счёте»): клиент сможет выполнить свой запрос *когда-нибудь*, когда получит, наконец, актуальные данные. В самом деле, согласованность в конечном счёте — скорее норма жизни для современных микросервисных архитектур, в которой может оказаться очень сложно как раз добиться обратного, т.е. строгой консистентности.
|
||||
|
||||
**NB**: на всякий случай уточним, что выбирать подходящий подход вы можете только в случае разработки новых API. Если вы уже предоставляете эндпойнт, реализующий какую-то модель консистентности, вы не можете понизить её уровень (в частности, сменить строгую консистентность на слабую), даже если вы никогда не документировали текущее поведение явно (мы обсудим это требование детальнее в главе [«О ватерлинии айсберга»](#back-compat-iceberg-waterline) раздела «Обратная совместимость»).
|
||||
|
||||
@ -39,7 +39,7 @@ const pendingOrders = await api.
|
||||
|
||||
Если мы не гарантируем сильную консистентность, то второй вызов может запросто вернуть пустой результат, ведь при чтении из реплики новый заказ мог просто до неё ещё не дойти.
|
||||
|
||||
Важный паттерн, который поможет в этой ситуации — это имплементация модели [«read-your-writes»](https://en.wikipedia.org/wiki/Consistency_model#Read-your-writes_consistency), а именно гарантии, что клиент всегда «видит» те изменения, которые сам же и внёс. Поднять уровень слабой консистентности до read-your-writes можно, если предложить клиенту самому передать токен, описывающий его последние изменения.
|
||||
Важный паттерн, который поможет в этой ситуации — это имплементация модели «read-your-writes»[ref Consistency Model. Read-Your-Writes Consistency](https://en.wikipedia.org/wiki/Consistency_model#Read-your-writes_consistency), а именно гарантии, что клиент всегда «видит» те изменения, которые сам же и внёс. Поднять уровень слабой консистентности до read-your-writes можно, если предложить клиенту самому передать токен, описывающий его последние изменения.
|
||||
|
||||
```
|
||||
const order = await api
|
||||
|
@ -56,7 +56,7 @@ const pendingOrders = await api.
|
||||
Асинхронный подход может применяться не только для устранения коллизий и неопределённости, но и для решения других прикладных задач:
|
||||
* организация ссылок на результаты операции и их кэширование (предполагается, что, если клиенту необходимо снова прочитать результат операции или же поделиться им с другим агентом, он может использовать для этого идентификатор задания);
|
||||
* обеспечение идемпотентности операций (для этого необходимо ввести подтверждение задания, и мы фактически получим схему с черновиками операции, описанную в главе [«Описание конечных интерфейсов»](#api-design-describing-interfaces));
|
||||
* нативное же обеспечение устойчивости к временному всплеску нагрузки на сервис — новые задачи встают в очередь (возможно, приоритизированную), фактически имплементируя [«маркерное ведро»](https://en.wikipedia.org/wiki/Token_bucket);
|
||||
* нативное же обеспечение устойчивости к временному всплеску нагрузки на сервис — новые задачи встают в очередь (возможно, приоритизированную), фактически имплементируя «маркерное ведро»[ref Token bucket](https://en.wikipedia.org/wiki/Token_bucket);
|
||||
* организация взаимодействия в тех случаях, когда время исполнения операции превышает разумные значения (в случае сетевых API — типичное время срабатывания сетевых таймаутов, т.е. десятки секунд) либо является непредсказуемым.
|
||||
|
||||
Кроме того, асинхронное взаимодействие удобнее с точки зрения развития API в будущем: устройство системы, обрабатывающей такие запросы, может меняться в сторону усложнения и удлинения конвейера исполнения задачи, в то время как синхронным функциям придётся укладываться в разумные временные рамки, чтобы оставаться синхронными — что, конечно, ограничивает возможности рефакторинга внутренних механик.
|
||||
|
@ -227,7 +227,7 @@ GET /v1/partners/{id}/offers/history⮠
|
||||
|
||||
Другим способом организации такого перебора может быть дата создания записи, но этот способ чуть сложнее в имплементации:
|
||||
* дата создания двух записей может полностью совпадать, особенно если записи могут массово генерироваться программно; в худшем случае может получиться так, что в один момент времени было создано больше записей, чем максимальный лимит их извлечения, и тогда часть записей вообще нельзя будет перебрать;
|
||||
* если хранилище данных поддерживает распределённую запись, то может оказаться, что более новая запись имеет чуть меньшую дату создания, нежели предыдущая известная (поскольку часы на разных виртуальных машинах могут идти чуть по-разному, и добиться хотя бы микросекундной точности крайне сложно[[1]](https://www.yugabyte.com/blog/evolving-clock-sync-for-distributed-databases/)), т.е. нарушится требование монотонности по признаку даты; если использование такого хранилища не имеет альтернативы, необходимо выбрать одно из двух зол:
|
||||
* если хранилище данных поддерживает распределённую запись, то может оказаться, что более новая запись имеет чуть меньшую дату создания, нежели предыдущая известная (поскольку часы на разных виртуальных машинах могут идти чуть по-разному, и добиться хотя бы микросекундной точности крайне сложно[ref Ranganathan, K. A Matter of Time: Evolving Clock Sync for Distributed Databases](https://www.yugabyte.com/blog/evolving-clock-sync-for-distributed-databases/), т.е. нарушится требование монотонности по признаку даты; если использование такого хранилища не имеет альтернативы, необходимо выбрать одно из двух зол:
|
||||
* внести рукотворные задержки, т.е. возвращать в API только элементы, созданные более чем N секунд назад — так, чтобы N было заведомо больше неравномерности хода часов (эта техника может использоваться и в тех случаях, когда список формируется асинхронно) — однако надо иметь в виду, что это решение вероятностное и всегда есть шанс отдачи неверных данных в случае проблем с синхронизацией на бэкенде;
|
||||
* описать нестабильность порядка новых элементов списка в документации и переложить решение этой проблемы на партнёров.
|
||||
|
||||
|
@ -15,13 +15,13 @@ GET /v1/orders/created-history⮠
|
||||
}
|
||||
```
|
||||
|
||||
Подобный паттерн (известный как [*поллинг*](https://en.wikipedia.org/wiki/Polling_(computer_science))) — наиболее часто встречающийся способ организации двунаправленной связи в API, когда партнёру требуется не только отправлять какие-то данные на сервер, но и получать оповещения от сервера об изменении какого-то состояния.
|
||||
Подобный паттерн (известный как «поллинг»[ref Polling (Computer Science)](https://en.wikipedia.org/wiki/Polling_(computer_science))) — наиболее часто встречающийся способ организации двунаправленной связи в API, когда партнёру требуется не только отправлять какие-то данные на сервер, но и получать оповещения от сервера об изменении какого-то состояния.
|
||||
|
||||
При всей простоте, поллинг всегда заставляет искать компромисс между отзывчивостью, производительностью и пропускной способностью системы:
|
||||
* чем длиннее интервал между последовательными запросами, тем больше будет задержка между изменением состояния на сервере и получением информации об этом на клиенте, и тем потенциально большим будет объём данных, которые необходимо будет передать за одну итерацию;
|
||||
* с другой стороны, чем этот интервал короче, чем большее количество запросов будет совершаться зря, т.к. никаких изменений в системе за прошедшее время не произошло.
|
||||
|
||||
Иными словами, поллинг всегда создаёт какой-то фоновый трафик в системе, но никогда не гарантирует максимальной отзывчивости. Иногда эту проблему решают с помощью «долгого поллинга» ([long polling](https://en.wikipedia.org/wiki/Push_technology#Long_polling)) — т.е. целенаправленно замедляют отдачу сервером ответа на длительное (секунды, десятки секунд) время до тех пор, пока на сервере не появится сообщение для передачи — однако мы не рекомендуем использовать этот подход в современных системах из-за связанных технических проблем (в частности, в условиях ненадёжной сети у клиента нет способа понять, что соединение на самом деле потеряно, и нужно отправить новый запрос, а не ожидать ответа на текущий).
|
||||
Иными словами, поллинг всегда создаёт какой-то фоновый трафик в системе, но никогда не гарантирует максимальной отзывчивости. Иногда эту проблему решают с помощью «долгого поллинга» (long polling) ([ref Long Polling](https://en.wikipedia.org/wiki/Push_technology#Long_polling)) — т.е. целенаправленно замедляют отдачу сервером ответа на длительное (секунды, десятки секунд) время до тех пор, пока на сервере не появится сообщение для передачи — однако мы не рекомендуем использовать этот подход в современных системах из-за связанных технических проблем (в частности, в условиях ненадёжной сети у клиента нет способа понять, что соединение на самом деле потеряно, и нужно отправить новый запрос, а не ожидать ответа на текущий).
|
||||
|
||||
Если оказывается, что обычного поллинга для решения пользовательских задач недостаточно, то можно перейти к обратной модели (*push*): сервер *сам* сообщает клиенту, что в системе произошли изменения.
|
||||
|
||||
@ -35,7 +35,7 @@ GET /v1/orders/created-history⮠
|
||||
|
||||
##### Дуплексные соединения
|
||||
|
||||
Самый очевидный вариант — использование технологий, позволяющих передавать по одному соединению сообщения в обе стороны. Наиболее известной из таких технологий является [WebSockets](https://websockets.spec.whatwg.org/). Иногда для организации полнодуплексного соединения применяется [Server Push, предусмотренный протоколом HTTP/2](https://datatracker.ietf.org/doc/html/rfc7540#section-8.2), однако надо отметить, что формально спецификация не предусматривает такого использования. Также существует протокол [WebRTC](https://www.w3.org/TR/webrtc/), но он, в основном, используется для обмена медиа-данными между клиентами, редко для клиент-серверного взаимодействия.
|
||||
Самый очевидный вариант — использование технологий, позволяющих передавать по одному соединению сообщения в обе стороны. Наиболее известной из таких технологий является WebSockets[ref WebSockets](https://websockets.spec.whatwg.org/). Иногда для организации полнодуплексного соединения применяется Server Push, предусмотренный протоколом HTTP/2[ref Hypertext Transfer Protocol Version 2 (HTTP/2). Server Push](https://datatracker.ietf.org/doc/html/rfc7540#section-8.2), однако надо отметить, что формально спецификация не предусматривает такого использования. Также существует протокол WebRTC[ref WebRTC](https://www.w3.org/TR/webrtc/), но он, в основном, используется для обмена медиа-данными между клиентами, редко для клиент-серверного взаимодействия.
|
||||
|
||||
Несмотря на то, что идея в целом выглядит достаточно простой и привлекательной, в реальности её использование довольно ограничено. Поддержки инициирования *сервером* отправки сообщения обратно на клиент практически нет в популярном серверном ПО и фреймворках (gRPC поддерживает потоки сообщений с сервера, но их всё равно должен инициировать клиент; использование потоков для пересылки сообщений по мере их возникновения — то же самое использование HTTP/2 Server Push в обход спецификации, что, фактически, работает как тот же самый long polling, только чуть более современный), и существующие стандарты спецификаций API также не поддерживают такой обмен данными: WebSockets является низкоуровневым протоколом, и формат взаимодействия придётся разработать самостоятельно.
|
||||
|
||||
@ -43,11 +43,11 @@ GET /v1/orders/created-history⮠
|
||||
|
||||
##### Раздельный канал обратного вызова
|
||||
|
||||
Вместо дуплексных соединений можно использовать два раздельных канала — один для отправки сообщений на сервер, другой для получения сообщений с сервера. Наиболее популярной технологией такого рода является [MQTT](https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html). Хотя эта технология считается максимально эффективной в силу использования низкоуровневых протоколов, её достоинства порождают и её недостатки:
|
||||
Вместо дуплексных соединений можно использовать два раздельных канала — один для отправки сообщений на сервер, другой для получения сообщений с сервера. Наиболее популярной технологией такого рода является MQTT[ref MQTT](https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html). Хотя эта технология считается максимально эффективной в силу использования низкоуровневых протоколов, её достоинства порождают и её недостатки:
|
||||
* технология в первую очередь предназначена для имплементации паттерна pub/sub и ценна наличием соответствующего серверного ПО (MQTT Broker); применить её для других задач, особенно для двунаправленного обмена данными, может быть сложно;
|
||||
* низкоуровневый протокол диктует необходимость разработки собственного формата данных.
|
||||
|
||||
Существует также веб-стандарт отправки серверных сообщений [Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html) (SSE). Однако по сравнению с WebSocket он менее функциональный (только текстовые данные, однонаправленный поток сообщений) и поэтому используется редко.
|
||||
Существует также веб-стандарт отправки серверных сообщений Server-Sent Events[ref Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html) (SSE). Однако по сравнению с WebSocket он менее функциональный (только текстовые данные, однонаправленный поток сообщений) и поэтому используется редко.
|
||||
|
||||
##### Сторонние сервисы отправки push-уведомлений
|
||||
|
||||
@ -88,7 +88,7 @@ GET /v1/orders/created-history⮠
|
||||
|
||||
##### 2. Договорённость о способах авторизации и аутентификации
|
||||
|
||||
Так как webhook-и представляют собой обратный канал взаимодействия, для него придётся разработать отдельный способ авторизации — это партнёр должен проверить, что запрос исходит от нашего бэкенда, а не наоборот. Мы повторяем здесь настоятельную рекомендацию не изобретать безопасность и использовать существующие стандартные механизмы, например, [mTLS](https://en.wikipedia.org/wiki/Mutual_authentication#mTLS), хотя в реальном мире с большой долей вероятности придётся использовать архаичные техники типа фиксации IP-адреса вызывающего сервера.
|
||||
Так как webhook-и представляют собой обратный канал взаимодействия, для него придётся разработать отдельный способ авторизации — это партнёр должен проверить, что запрос исходит от нашего бэкенда, а не наоборот. Мы повторяем здесь настоятельную рекомендацию не изобретать безопасность и использовать существующие стандартные механизмы, например, mTLS[ref Mutual Authentication. mTLS](https://en.wikipedia.org/wiki/Mutual_authentication#mTLS), хотя в реальном мире с большой долей вероятности придётся использовать архаичные техники типа фиксации IP-адреса вызывающего сервера.
|
||||
|
||||
##### 3. API для задания адреса webhook-а
|
||||
|
||||
@ -97,7 +97,7 @@ GET /v1/orders/created-history⮠
|
||||
**Важно**. К операции задания адреса callback-а нужно подходить с максимально возможной серьёзностью (очень желательно требовать второй фактор авторизации для подтверждения этой операции), поскольку, получив доступ к такой функциональности, злоумышленник может совершить множество весьма неприятных атак:
|
||||
* если указать в качестве приёмника сторонний URL, можно получить доступ к потоку всех заказов партнёра и при этом вызвать перебои в его работе;
|
||||
* такая уязвимость может также эксплуатироваться с целью организации DoS-атаки на сторонние сервисы;
|
||||
* если указать в качестве webhook-а URL интранет-сервисов компании-провайдера API, можно осуществить [SSRF-атаку](https://en.wikipedia.org/wiki/SSRF) на инфраструктуру самой компании.
|
||||
* если указать в качестве webhook-а URL интранет-сервисов компании-провайдера API, можно осуществить SSRF-атаку[ref SSRF](https://en.wikipedia.org/wiki/SSRF) на инфраструктуру самой компании.
|
||||
|
||||
#### Типичные проблемы интеграции через webhook
|
||||
|
||||
@ -115,7 +115,7 @@ GET /v1/orders/created-history⮠
|
||||
1. Состояние системы должно быть восстановимо. Даже если партнёр неправильно обработал сообщения, всегда должна быть возможность реабилитироваться и получить список последних событий и/или полное состояние системы, чтобы исправить случившиеся ошибки.
|
||||
2. Помогите партнёру написать правильный код, зафиксировав в документации неочевидные моменты, с которыми могут быть незнакомы неопытные разработчики:
|
||||
* ключи идемпотентности каждой операции;
|
||||
* гарантии доставки (exactly once, at least once; [см. описание гарантий доставки](https://docs.confluent.io/kafka/design/delivery-semantics.html) на примере технологии Apache Kafka);
|
||||
* гарантии доставки (exactly once, at least once; см. описание гарантий доставки[ref Apache Kafka. Kafka Design. Message Delivery Guarantees](https://docs.confluent.io/kafka/design/delivery-semantics.html) на примере технологии Apache Kafka);
|
||||
* будет ли сервер генерировать параллельные запросы к webhook-у и, если да, каково максимальное количество одновременных запросов;
|
||||
* гарантирует ли сервер строгий порядок сообщений (запросы всегда доставляются в порядке от самого старого к самому новому)
|
||||
* размеры полей и сообщений в байтах;
|
||||
@ -126,11 +126,11 @@ GET /v1/orders/created-history⮠
|
||||
|
||||
#### Очереди сообщений
|
||||
|
||||
Для внутренних API технология webhook-ов (то есть наличия программной возможности задавать URL обратного вызова) либо вовсе не нужна, либо решается с помощью протоколов [Service Discovery](https://en.wikipedia.org/wiki/Web_Services_Discovery), поскольку сервисы в составе одного бэкенда как правило равноправны — если сервис А может вызывать сервис Б, то и сервис Б может вызывать сервис А.
|
||||
Для внутренних API технология webhook-ов (то есть наличия программной возможности задавать URL обратного вызова) либо вовсе не нужна, либо решается с помощью протоколов Service Discovery[ref Web Services Discovery](https://en.wikipedia.org/wiki/Web_Services_Discovery), поскольку сервисы в составе одного бэкенда как правило равноправны — если сервис А может вызывать сервис Б, то и сервис Б может вызывать сервис А.
|
||||
|
||||
Однако все проблемы Webhook-ов, описанные нами выше, для таких обратных вызовов всё ещё актуальны. Вызов внутреннего сервиса всё ещё может окончиться false negative-ошибкой, внутренние клиенты могут не ожидать нарушения порядка пересылки сообщений и так далее.
|
||||
|
||||
Для решения этих проблем, а также для большей горизонтальной масштабируемости технологий обратного вызова, были созданы [сервисы очередей сообщений](https://en.wikipedia.org/wiki/Message_queue) и, в частности, различные серверные реализации паттерна pub/sub. В настоящий момент pub/sub-архитектуры пользуются большой популярностью среди разработчиков, вплоть до перевода любого межсервисного взаимодействия на очереди событий.
|
||||
Для решения этих проблем, а также для большей горизонтальной масштабируемости технологий обратного вызова, были созданы сервисы очередей сообщений[ref Message Queue](https://en.wikipedia.org/wiki/Message_queue) и, в частности, различные серверные реализации паттерна pub/sub. В настоящий момент pub/sub-архитектуры пользуются большой популярностью среди разработчиков, вплоть до перевода любого межсервисного взаимодействия на очереди событий.
|
||||
|
||||
**NB**: отметим, что ничего бесплатного в мире не бывает, и за эти гарантии доставки и горизонтальную масштабируемость необходимо платить:
|
||||
* межсерверное взаимодействие становится событийно-консистентным со всеми вытекающими отсюда проблемами;
|
||||
|
@ -187,7 +187,7 @@ POST /v1/bulk-status-change
|
||||
Если операции в списке как-то зависят одна от другой (как в примере выше — партнёру нужно *и* сделать рефанд, *и* отменить заказ, выполнение только одной из этих операций бессмысленно) либо важен порядок исполнения операций, неатомарные эндпойнты будут постоянно приводить к проблемам. И даже если вам кажется, что в вашей предметной области таких проблем нет, в какой-то момент может оказаться, что вы чего-то не учли.
|
||||
|
||||
Поэтому наши рекомендации по организации эндпойнтов массовых изменений таковы:
|
||||
1. Если вы можете обойтись без таких эндпойнтов — обойдитесь. В server-to-server интеграциях экономия копеечная, в современных сетях с поддержкой протокола [QUIC](https://datatracker.ietf.org/doc/html/rfc9000) и мультиплексирования запросов тоже весьма сомнительная.
|
||||
1. Если вы можете обойтись без таких эндпойнтов — обойдитесь. В server-to-server интеграциях экономия копеечная, в современных сетях с поддержкой протокола QUIC[ref QUIC](https://datatracker.ietf.org/doc/html/rfc9000) и мультиплексирования запросов тоже весьма сомнительная.
|
||||
2. Если такой эндпойнт всё же нужен, лучше реализовать его атомарно и предоставить SDK, которые помогут партнёрам не допускать типичные ошибки.
|
||||
3. Если реализовать атомарный эндпойнт невозможно, тщательно продумайте дизайн API, чтобы не допустить ошибок, подобных описанным выше.
|
||||
4. Вне зависимости от выбранного подхода, ответы сервера должны включать разбивку по подзапросам. В случае атомарных эндпойнтов это означает включение в ответ списка ошибок, из-за которых исполнение запроса не удалось, в идеале — со всеми потенциальными ошибками (т.е. с результатами проверок каждого подзапроса на валидность). Для неатомарных эндпойнтов необходимо возвращать список со статусами каждого подзапроса и всеми возникшими ошибками.
|
||||
|
@ -91,7 +91,7 @@ PATCH /v1/orders/{id}
|
||||
* наконец, подобная наивная концепция организации совместного доступа работает ровно до того момента, пока изменения транзитивны, т.е. результат не зависит от порядка выполнения операций (в нашим примере это уже не так — операции удаления первого элемента и редактирования первого элемента нетранзитивны);
|
||||
* кроме того, часто в рамках той же концепции экономят и на исходящем трафике, возвращая пустой ответ сервера для модифицирующих операций; таким образом, два клиента, редактирующих одну и ту же сущность, не видят изменения друг друга, что ещё больше повышает вероятность получить совершенно неожиданные результаты.
|
||||
|
||||
Это решение можно улучшить путём ввода явных управляющих конструкций вместо «магических значений» и введением мета-опций операции (скажем, фильтра по именам полей, как это [принято в gRPC](https://protobuf.dev/reference/protobuf/google.protobuf/#field-masks-updates)), например, так:
|
||||
Это решение можно улучшить путём ввода явных управляющих конструкций вместо «магических значений» и введением мета-опций операции (скажем, фильтра по именам полей, как это принято в gRPC поверх Protobuf[ref Protocol Buffers. Field Masks in Update Operations](https://protobuf.dev/reference/protobuf/google.protobuf/#field-masks-updates)), например, так:
|
||||
|
||||
```
|
||||
// Частично перезаписывает заказ:
|
||||
@ -228,4 +228,4 @@ X-Idempotency-Token: <токен>
|
||||
|
||||
Этот подход существенно сложнее в имплементации, но является единственным возможным вариантом реализации совместного редактирования, поскольку он явно отражает, что в действительности делал пользовать с представлением объекта. Имея данные в таком формате возможно организовать и оффлайн-редактирование, когда пользовательские изменения накапливаются и сервер впоследствии автоматически разрешает возможные конфликты, основываясь на истории ревизий.
|
||||
|
||||
**NB**: один из подходов к этой задаче — разработка такой номенклатуры операций над данными (например, [CRDT](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type)), в которой любые действия транзитивны (т.е. конечное состояние системы не зависит от того, в каком порядке они были применены). Мы, однако, склонны считать такой подход применимым только к весьма ограниченным предметным областям — поскольку в реальной жизни нетранзитивные действия находятся почти всегда. Если один пользователь ввёл в документ новый текст, а другой пользователь удалил документ — никакого разумного (т.е. удовлетворительного с точки зрения обоих акторов) способа автоматического разрешения конфликта здесь нет, необходимо явно спросить пользователей, что бы они хотели сделать с возникшим конфликтом.
|
||||
**NB**: один из подходов к этой задаче — разработка такой номенклатуры операций над данными (например, conflict-free replicated data type (*CRDT*)[ref Conflict-Free Replicated Data Type](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type)), в которой любые действия транзитивны (т.е. конечное состояние системы не зависит от того, в каком порядке они были применены). Мы, однако, склонны считать такой подход применимым только к весьма ограниченным предметным областям — поскольку в реальной жизни нетранзитивные действия находятся почти всегда. Если один пользователь ввёл в документ новый текст, а другой пользователь удалил документ — никакого разумного (т.е. удовлетворительного с точки зрения обоих акторов) способа автоматического разрешения конфликта здесь нет, необходимо явно спросить пользователей, что бы они хотели сделать с возникшим конфликтом.
|
||||
|
@ -6,7 +6,7 @@
|
||||
2. Вышестоящие сущности должны при этом оставаться информационными контекстами для нижестоящих, т.е. не предписывать конкретное поведение, а только сообщать о своём состоянии и предоставлять функциональность для его изменения (прямую через соответствующие методы либо косвенную через получение определённых событий).
|
||||
3. Конкретная функциональность, т.е. работа непосредственно с «железом», нижележащим API платформы, должна быть делегирована сущностям самого низкого уровня.
|
||||
|
||||
**NB**. В этих правилах нет ничего особенно нового: в них легко опознаются принципы архитектуры [SOLID](https://en.wikipedia.org/wiki/SOLID) — что неудивительно, поскольку SOLID концентрируется на контрактно-ориентированном подходе к разработке, а API по определению и есть контракт. Мы лишь добавляем в эти принципы понятие уровней абстракции и информационных контекстов.
|
||||
**NB**. В этих правилах нет ничего особенно нового: в них легко опознаются принципы архитектуры SOLID[ref SOLID](https://en.wikipedia.org/wiki/SOLID)[ref:{"short":"Martin, R. C.","extra":["Design Principles and Design Patterns"]}](http://staff.cs.utu.fi/~jounsmed/doos_06/material/DesignPrinciplesAndPatterns.pdf) — что неудивительно, поскольку SOLID концентрируется на контрактно-ориентированном подходе к разработке, а API по определению и есть контракт. Мы лишь добавляем в эти принципы понятие уровней абстракции и информационных контекстов.
|
||||
|
||||
Остаётся, однако, неотвеченным вопрос о том, как изначально выстроить номенклатуру сущностей таким образом, чтобы расширение API не превращало её в мешанину из различных неконсистентных методов разных эпох. Впрочем, ответ на него довольно очевиден: чтобы при абстрагировании не возникало неловких ситуаций, подобно рассмотренному нами примеру с полями рецептов, все сущности необходимо *изначально* рассматривать как частную реализацию некоторого более общего интерфейса, даже если никаких альтернативных реализаций в настоящий момент не предвидится.
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
Удобной (и опять же имеющей почти стопроцентное проникновение) абстракцией над IP-адресами оказалась система доменных имён, позволяющий назначить узлам сети человекочитаемые синонимы.
|
||||
|
||||
Появление доменных имён потребовало разработки клиент-серверных протоколов более высокого, чем TCP/IP, уровня, и для передачи текстовых (гипертекстовых) данных таким протоколом стал [HTTP 0.9](https://www.w3.org/Protocols/HTTP/AsImplemented.html), разработанный Тимом Бёрнерсом-Ли опубликованный в 1991 году. Помимо поддержки обращения к узлам сети по именам, HTTP также предоставил ещё одну очень удобную абстракцию, а именно назначение собственных адресов эндпойнтам, работающим на одном сетевом узле.
|
||||
Появление доменных имён потребовало разработки клиент-серверных протоколов более высокого, чем TCP/IP, уровня, и для передачи текстовых (гипертекстовых) данных таким протоколом стал HTTP 0.9[ref The Original HTTP as defined in 1991](https://www.w3.org/Protocols/HTTP/AsImplemented.html), разработанный Тимом Бёрнерсом-Ли опубликованный в 1991 году. Помимо поддержки обращения к узлам сети по именам, HTTP также предоставил ещё одну очень удобную абстракцию, а именно назначение собственных адресов эндпойнтам, работающим на одном сетевом узле.
|
||||
|
||||
Протокол был очень прост и всего лишь описывал способ получить документ, открыв TCP/IP соединение с сервером и передав строку вида `GET адрес_документа`. Позднее протокол был дополнен стандартом URL, позволяющим детализировать адрес документа, и далее протокол начал развиваться стремительно: появились новые глаголы помимо `GET`, статусы ответов, заголовки, типы данных и так далее.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
### [Преимущества и недостатки HTTP API в сравнении с альтернативными технологиями][http-api-pros-and-cons]
|
||||
|
||||
По прочтению предыдущей главы у читателя может возникнуть резонный вопрос — а почему вообще существует такая дихотомия: одни API полагаются на стандартную семантику HTTP, другие полностью от неё отказываются в пользу новоизобретённых стандартов, а третьи существуют где-то посередине. Например, если мы посмотрим на [формат ответа в JSON-RPC](https://www.jsonrpc.org/specification#response_object), то мы обнаружим, что он легко мог бы быть заменён на стандартные средства протокола HTTP. Вместо
|
||||
По прочтению предыдущей главы у читателя может возникнуть резонный вопрос — а почему вообще существует такая дихотомия: одни API полагаются на стандартную семантику HTTP, другие полностью от неё отказываются в пользу новоизобретённых стандартов, а третьи существуют где-то посередине. Например, если мы посмотрим на формат ответа в JSON-RPC[ref JSON-RPC 2.0 Specification. Response object](https://www.jsonrpc.org/specification#response_object), то мы обнаружим, что он легко мог бы быть заменён на стандартные средства протокола HTTP. Вместо
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
@ -71,11 +71,11 @@ HTTP/1.1 200 OK
|
||||
|
||||
Будем здесь честны: большинство существующих имплементаций HTTP API действительно страдают от указанных проблем. Тем не менее, мы берём на себя смелость заявить, что все эти проблемы большей частью надуманы, и их решению не уделяют большого внимания потому, что указанные накладные расходы не являются сколько-нибудь заметными для большинства вендоров API. В частности:
|
||||
|
||||
1. Если мы говорим об избыточности формата, то необходимо сделать важную оговорку: всё вышесказанное верно, если мы не применяем сжатие. [Сравнения показывают](https://nilsmagnus.github.io/post/proto-json-sizes/), что использование gzip практически нивелирует разницу в размере JSON документов относительно альтернативных бинарных форматов (а есть ещё и специально предназначенные для текстовых данных архиваторы, например, [brotli](https://datatracker.ietf.org/doc/html/rfc7932)).
|
||||
1. Если мы говорим об избыточности формата, то необходимо сделать важную оговорку: всё вышесказанное верно, если мы не применяем сжатие. Сравнения показывают[ref Comparing sizes of protobuf vs json](https://nilsmagnus.github.io/post/proto-json-sizes/), что использование gzip практически нивелирует разницу в размере JSON документов относительно альтернативных бинарных форматов (а есть ещё и специально предназначенные для текстовых данных архиваторы, например, brotli[ref Brotli Compressed Data Format](https://datatracker.ietf.org/doc/html/rfc7932)).
|
||||
|
||||
2. Вообще говоря, если такая нужда появляется, то и в рамках HTTP API вполне можно регулировать список возвращаемых полей ответа, это вполне соответствует духу и букве стандарта. Однако, мы должны заметить, что экономия трафика на возврате частичных состояний (которую мы рассматривали подробно в главе «[Частичные обновления](#api-patterns-partial-updates)») очень редко бывает оправдана.
|
||||
|
||||
3. Если использовать стандартные десериализаторы JSON, разница по сравнению с бинарными форматами может оказаться действительно очень большой. Если, однако, эти накладные расходы являются проблемой, стоит обратиться к альтернативным десериализаторам — в частности, [simdjson](https://github.com/simdjson/simdjson). Благодаря оптимизированному низкоуровневому коду simdjson показывает отличную производительность, которой может не хватить только совсем уж экзотическим API.
|
||||
3. Если использовать стандартные десериализаторы JSON, разница по сравнению с бинарными форматами может оказаться действительно очень большой. Если, однако, эти накладные расходы являются проблемой, стоит обратиться к альтернативным десериализаторам — в частности, simdjson[ref simdjson : Parsing gigabytes of JSON per second](https://github.com/simdjson/simdjson). Благодаря оптимизированному низкоуровневому коду simdjson показывает отличную производительность, которой может не хватить только совсем уж экзотическим API.
|
||||
|
||||
4. Вообще говоря, парадигма HTTP API подразумевает, что для бинарных данных (такие как изображения или видеофайлы) предоставляются отдельные эндпойнты. Передача бинарных данных в теле JSON-ответа необходима только в случаях, когда отдельный запрос за ними представляет собой проблему с точки зрения производительности. Такой проблемы фактически не существует в server-2-server взаимодействии и в протоколе HTTP 2.0 и выше.
|
||||
|
||||
@ -85,7 +85,7 @@ HTTP/1.1 200 OK
|
||||
|
||||
#### Преимущества и недостатки формата JSON
|
||||
|
||||
Как нетрудно заметить, большинство претензий, предъявляемых к концепции HTTP API, относятся вовсе не к HTTP, а к использованию формата JSON. В самом деле, ничто не мешает разработать API, которое будет использовать любой бинарный формат вместо JSON (включая те же [Protocol Buffers](https://protobuf.dev/)), и тогда разница между Protobuf-over-HTTP API и gRPC сведётся только к (не)использованию подробных URL, статус-кодов и заголовков запросов и ответов (и вытекающей отсюда (не)возможности использовать то или иное стандартное программное обеспечение «из коробки»).
|
||||
Как нетрудно заметить, большинство претензий, предъявляемых к концепции HTTP API, относятся вовсе не к HTTP, а к использованию формата JSON. В самом деле, ничто не мешает разработать API, которое будет использовать любой бинарный формат вместо JSON (включая те же Protocol Buffers), и тогда разница между Protobuf-over-HTTP API и gRPC сведётся только к (не)использованию подробных URL, статус-кодов и заголовков запросов и ответов (и вытекающей отсюда (не)возможности использовать то или иное стандартное программное обеспечение «из коробки»).
|
||||
|
||||
Однако, во многих случаях (включая настоящую книгу) разработчики предпочитают текстовый JSON бинарным Protobuf (Flatbuffers, Thrift, Avro и т.д.) по очень простой причине: JSON очень легко и удобно читать. Во-первых, он текстовый и не требует дополнительной расшифровки; во-вторых, имена полей включены в сам файл. Если сообщение в формате protobuf невозможно прочитать без `.proto`-файла, то по JSON-документу почти всегда можно попытаться понять, что за данные в нём описаны. В совокупности с тем, что при разработке HTTP API мы также стараемся следовать стандартной семантике всей остальной обвязки, в итоге мы получаем API, запросы и ответы к которому (по крайней мере в теории) удобно читаются и интуитивно понятны.
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Прежде, чем перейти непосредственно к паттернам проектирования HTTP API, мы должны сделать ещё одно терминологическое отступление. Очень часто HTTP API, соответствующие данному нами в главе «[О концепции HTTP API и терминологии](#http-api-concepts)» определению, называют «REST API» или «RESTful API». В настоящем разделе мы эти термины не используем, поскольку оба этих термина неформальные и не несут конкретного смысла.
|
||||
|
||||
Что такое «REST»? В 2000 году один из авторов спецификаций HTTP и URI Рой Филдинг защитил докторскую диссертацию на тему «Архитектурные стили и дизайн архитектуры сетевого программного обеспечения», пятая глава которой была озаглавлена как «[Representational State Transfer (REST)](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm)».
|
||||
Что такое «REST»? В 2000 году один из авторов спецификаций HTTP и URI Рой Филдинг защитил докторскую диссертацию на тему «Архитектурные стили и дизайн архитектуры сетевого программного обеспечения», пятая глава которой была озаглавлена как «Representational State Transfer (REST)»[ref:{"short":"Fielding, R. (2000)","extra":["Architectural Styles and the Design of Network-based Software Architectures","Representational State Transfer (REST)"]}](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm).
|
||||
|
||||
Как нетрудно убедиться, прочитав эту главу, она представляет собой абстрактный обзор распределённой сетевой архитектуры, вообще не привязанной ни к HTTP, ни к URL. Более того, она вовсе не посвящена правилам дизайна API — в этой главе Филдинг методично *перечисляет ограничения*, с которыми приходится сталкиваться разработчику распределённого сетевого программного обеспечения. Вот они:
|
||||
|
||||
@ -22,19 +22,19 @@
|
||||
* раз есть интерфейс взаимодействия, значит, под него всегда можно мимикрировать, а значит, требование независимости имплементации клиента и сервера всегда выполнимо;
|
||||
* раз можно сделать альтернативную имплементацию сервера — значит, можно сделать и многослойную архитектуру, поставив дополнительный прокси между клиентом и сервером;
|
||||
* поскольку клиент представляет собой вычислительную машину, он всегда хранит хоть какое-то состояние и кэширует хоть какие-то данные;
|
||||
* наконец, code-on-demand вообще лукавое требование, поскольку в [архитектуре фон Неймана](https://en.wikipedia.org/wiki/Von_Neumann_architecture) всегда можно объявить данные, полученные по сети, «инструкциями» на некотором формальном языке, а код клиента — их интерпретатором.
|
||||
* наконец, code-on-demand вообще лукавое требование, поскольку в архитектуре фон Неймана[ref Von Neumann Architecture](https://en.wikipedia.org/wiki/Von_Neumann_architecture) всегда можно объявить данные, полученные по сети, «инструкциями» на некотором формальном языке, а код клиента — их интерпретатором.
|
||||
|
||||
Да, конечно, вышеприведённое рассуждение является софизмом, доведением до абсурда. Самое забавное в этом упражнении состоит в том, что мы можем довести его до абсурда и в другую сторону, объявив ограничения REST неисполнимыми. Например, очевидно, что требование code-on-demand противоречит требованию независимости клиента и сервера — клиент должен уметь интерпретировать код с сервера, написанный на вполне конкретном языке. Что касается правила на букву S («stateless»), то систем, в которых сервер *вообще не хранит никакого контекста клиента* в мире вообще практически нет, поскольку почти ничего полезного для клиента в такой системе сделать нельзя. (Чего, кстати, Филдинг прямым текстом требует: «коммуникация … не может получать никаких преимуществ от того, что на сервере хранится какой-то контекст».)
|
||||
|
||||
Наконец, сам Филдинг внёс дополнительную энтропию в вопрос, выпустив в 2008 году [разъяснение](https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven), что же он имел в виду. В частности, в этой статье утверждается, что:
|
||||
Наконец, сам Филдинг внёс дополнительную энтропию в вопрос, выпустив в 2008 году разъяснение[ref Fielding, R. T. REST APIs must be hypertext-driven](https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven), что же он имел в виду. В частности, в этой статье утверждается, что:
|
||||
* разработка REST API должна фокусироваться на описании медиатипов, представляющих ресурсы; при этом клиент вообще ничего про эти медиатипы знать не должен;
|
||||
* в REST API не должно быть фиксированных имён ресурсов и операций над ними, клиент должен извлекать эту информацию из ответов сервера.
|
||||
|
||||
REST по Филдингу-2008 подразумевает, что клиент, получив каким-то образом ссылку на точку входа в REST API, далее должен быть в состоянии полностью выстроить взаимодействие с API, не обладая вообще никаким априорным знанием о нём, и уж тем более не должен содержать никакого специально написанного кода для работы с этим API. Это требование — гораздо более сильное, нежели принципы, описанные в диссертации 2000 года. В частности, из идеи REST-2008 вытекает отсутствие фиксированных шаблонов URL для выполнения операций над ресурсами — предполагается, что такие URL присутствуют в виде гиперссылок в представлениях ресурсов (эта концепция известна также под названием [HATEOAS](https://en.wikipedia.org/wiki/HATEOAS)). Диссертация же 2000 года никаких строгих определений «гипермедиа», которые препятствовали бы идее конструирования ссылок на основе априорных знаний об API (например, по спецификации), не содержит.
|
||||
REST по Филдингу-2008 подразумевает, что клиент, получив каким-то образом ссылку на точку входа в REST API, далее должен быть в состоянии полностью выстроить взаимодействие с API, не обладая вообще никаким априорным знанием о нём, и уж тем более не должен содержать никакого специально написанного кода для работы с этим API. Это требование — гораздо более сильное, нежели принципы, описанные в диссертации 2000 года. В частности, из идеи REST-2008 вытекает отсутствие фиксированных шаблонов URL для выполнения операций над ресурсами — предполагается, что такие URL присутствуют в виде гиперссылок в представлениях ресурсов (эта концепция известна также под названием HATEOAS[ref HATEOAS](https://en.wikipedia.org/wiki/HATEOAS)). Диссертация же 2000 года никаких строгих определений «гипермедиа», которые препятствовали бы идее конструирования ссылок на основе априорных знаний об API (например, по спецификации), не содержит.
|
||||
|
||||
**NB**: оставляя за скобками тот факт, что Филдинг весьма вольно истолковал свою собственную диссертацию, просто отметим, что ни одна существующая система в мире не удовлетворяет описанию REST по Филдингу-2008.
|
||||
|
||||
Нам неизвестно, почему из всех обзоров абстрактной сетевой архитектуры именно концепция Филдинга обрела столь широкую популярность; очевидно другое: теория Филдинга, преломившись в умах миллионов программистов (включая самого Филдинга), превратилась в целую инженерную субкультуру. Путём редукции абстракций REST применительно конкретно к протоколу HTTP и стандарту URL родилась химера «RESTful API», [конкретного смысла которой никто не знает](https://restfulapi.net/).
|
||||
Нам неизвестно, почему из всех обзоров абстрактной сетевой архитектуры именно концепция Филдинга обрела столь широкую популярность; очевидно другое: теория Филдинга, преломившись в умах миллионов программистов (включая самого Филдинга), превратилась в целую инженерную субкультуру. Путём редукции абстракций REST применительно конкретно к протоколу HTTP и стандарту URL родилась химера «RESTful API», конкретного смысла которой никто не знает[ref Gupta, L. What is REST](https://restfulapi.net/).
|
||||
|
||||
Хотим ли мы тем самым сказать, что REST является бессмысленной концепцией? Отнюдь нет. Мы только хотели показать, что она допускает чересчур широкую интерпретацию, в чём одновременно кроется и её сила, и её слабость.
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Важное подготовительное упражнение, которое мы должны сделать — это дать описание формата HTTP-запросов и ответов и прояснить базовые понятия. Многое из написанного ниже может показаться читателю самоочевидным, но, увы, специфика протокола такова, что даже базовые сведения о нём, без которых мы не сможем двигаться дальше, разбросаны по обширной и фрагментированной документации, и даже опытные разработчики могут не знать тех или иных нюансов. Ниже мы попытаемся дать структурированный обзор протокола в том объёме, который необходим нам для проектирования HTTP API.
|
||||
|
||||
В описании семантики и формата протокола мы будем руководствоваться свежевышедшим [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html), который заменил аж девять предыдущих спецификаций, описывавших разные аспекты технологии (при этом большое количество различной дополнительной функциональности всё ещё покрывается отдельными стандартами. В частности, принципы HTTP-кэширования описаны в отдельном [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html), а широко используемый в API метод `PATCH` так и не вошёл в основной RFC и регулируется [RFC 5789](https://www.rfc-editor.org/rfc/rfc5789.html)).
|
||||
В описании семантики и формата протокола мы будем руководствоваться свежевышедшим RFC 9110[ref RFC 9110 HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html), который заменил аж девять предыдущих спецификаций, описывавших разные аспекты технологии (при этом большое количество различной дополнительной функциональности всё ещё покрывается отдельными стандартами. В частности, принципы HTTP-кэширования описаны в отдельном RFC 9111[ref RFC 9111 HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111.html), а широко используемый в API метод `PATCH` так и не вошёл в основной RFC и регулируется RFC 5789[ref PATCH Method for HTTP](https://www.rfc-editor.org/rfc/rfc5789.html)).
|
||||
|
||||
HTTP-запрос представляет собой (1) применение определённого глагола к URL с (2) указанием версии протокола, (3) передачей дополнительной мета-информации в заголовках и, возможно, (4) каких-то данных в теле запроса:
|
||||
|
||||
@ -33,13 +33,13 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
**NB**: в HTTP/2 (и будущем HTTP/3) вместо единого текстового формата используются отдельные бинарные фреймы для передачи заголовков и данных. Этот факт не влияет на излагаемые архитектурные принципы, но во избежание двусмысленности мы будем давать примеры в формате HTTP/1.1. Подробнее о формате HTTP/2 можно прочитать [здесь](https://hpbn.co/http2/).
|
||||
**NB**: в HTTP/2 (и будущем HTTP/3) вместо единого текстового формата используются отдельные бинарные фреймы для передачи заголовков и данных[ref:{"short":"Grigorik, I. (2013)","extra":["High Performance Browser Networking","Chapter 12. HTTP/2"]}](https://hpbn.co/http2/). Этот факт не влияет на излагаемые архитектурные принципы, но во избежание двусмысленности мы будем давать примеры в формате HTTP/1.1.
|
||||
|
||||
##### URL
|
||||
|
||||
URL — единица адресации в HTTP API (некоторые евангелисты технологии даже используют термин «пространство URL» как синоним для Мировой паутины). Предполагается, что HTTP API должен использовать систему адресов столь же гранулярную, как и предметная область; иными словами, у любых сущностей, которыми мы можем манипулировать независимо, должен быть свой URL.
|
||||
|
||||
Формат URL регулируется [отдельным стандартом](https://url.spec.whatwg.org/), который развивает независимое сообщество Web Hypertext Application Technology Working Group (WHATWG). Считается, что концепция URL (вместе с понятием универсального имени ресурса, URN) составляет более общую сущность URI (универсальный идентификатор ресурса). (Разница между URL и URN заключается в том, что URL позволяет *найти* некоторый ресурс в рамках некоторого протокола доступа, в то время как URN — «внутреннее» имя объекта, которое само по себе никак не помогает получить к нему доступ.)
|
||||
Формат URL регулируется отдельным стандартом[ref URL Living Standard](https://url.spec.whatwg.org/), который развивает независимое сообщество Web Hypertext Application Technology Working Group (WHATWG). Считается, что концепция URL (вместе с понятием универсального имени ресурса, URN) составляет более общую сущность URI (универсальный идентификатор ресурса). (Разница между URL и URN заключается в том, что URL позволяет *найти* некоторый ресурс в рамках некоторого протокола доступа, в то время как URN — «внутреннее» имя объекта, которое само по себе никак не помогает получить к нему доступ.)
|
||||
|
||||
URL принято раскладывать на составляющие, каждая из которых опциональна. В стандарте перечислены разнообразные исторические наслоения (например, передача логинов и паролей в URL или использование не-UTF кодировки), которые мы опустим. В рамках дизайна HTTP API нам интересны следующие компоненты:
|
||||
* схема (scheme) — протокол обращения (в нашем случае всегда `https:`);
|
||||
@ -69,7 +69,7 @@ URL принято раскладывать на составляющие, ка
|
||||
|
||||
Важное свойство заголовков — это возможность считывать их до того, как получено тело сообщения. Таким образом, заголовки могут, во-первых, сами по себе влиять на обработку запроса или ответа, и ими можно относительно легко манипулировать при проксировании — и многие сетевые агенты действительно это делают, добавляя или модифицируя заголовки по своему усмотрению (в частности, современные веб-браузеры добавляют к запросам целую коллекцию заголовков: `User-Agent`, `Origin`, `Accept-Language`, `Connection`, `Referer`, `Sec-Fetch-*` и так далее, а современное ПО веб-серверов, в свою очередь, автоматически добавляет или модифицирует такие заголовки как `X-Powered-By`, `Date`, `Content-Length`, `Content-Encoding`, `X-Forwarded-For`).
|
||||
|
||||
Подобное вольное обращение с заголовками создаёт определённые проблемы, если ваш API предусматривает передачу дополнительных полей метаданных, поскольку придуманные вами имена полей могут случайно совпасть с какими-то из существующих стандартных имён (или ещё хуже — в будущем появится новое стандартное поле, совпадающее с вашим). Долгое время во избежание подобных коллизий использовался префикс `X-`; уже более 10 лет как эта практика объявлена устаревшей и не рекомендуется к использованию (см. подробный разбор вопроса в [RFC 6648](https://www.rfc-editor.org/rfc/rfc6648)), однако отказа от этого префикса по факту не произошло (и многие широко распространённые нестандартные заголовки, например, `X-Forwarded-For`, его всё ещё содержат). Таким образом, использование префикса `X-` вероятность коллизий снижает, но не устраняет. Тот же RFC вполне разумно предлагает использовать вместо `X-` префикс в виде имени компании. (Мы со своей стороны склонны рекомендовать использовать оба префикса в формате `X-ApiName-FieldName`; префикс `X-` для читабельности [чтобы отличать специальные заголовки от стандартных], а префикс с именем компании или API — чтобы не произошло коллизий с каким-нибудь другим нестандартным префиксом.)
|
||||
Подобное вольное обращение с заголовками создаёт определённые проблемы, если ваш API предусматривает передачу дополнительных полей метаданных, поскольку придуманные вами имена полей могут случайно совпасть с какими-то из существующих стандартных имён (или ещё хуже — в будущем появится новое стандартное поле, совпадающее с вашим). Долгое время во избежание подобных коллизий использовался префикс `X-`; уже более 10 лет как эта практика объявлена устаревшей и не рекомендуется к использованию (см. подробный разбор вопроса в RFC 6648[ref Deprecating the "X-" Prefix and Similar Constructs in Application Protocols](https://www.rfc-editor.org/rfc/rfc6648)), однако отказа от этого префикса по факту не произошло (и многие широко распространённые нестандартные заголовки, например, `X-Forwarded-For`, его всё ещё содержат). Таким образом, использование префикса `X-` вероятность коллизий снижает, но не устраняет. Тот же RFC вполне разумно предлагает использовать вместо `X-` префикс в виде имени компании. (Мы со своей стороны склонны рекомендовать использовать оба префикса в формате `X-ApiName-FieldName`; префикс `X-` для читабельности [чтобы отличать специальные заголовки от стандартных], а префикс с именем компании или API — чтобы не произошло коллизий с каким-нибудь другим нестандартным префиксом.)
|
||||
|
||||
Помимо прочего заголовки используются как управляющие конструкции — это т.н. «content negotiation», т.е. договорённость клиента и сервера о формате ответа (через заголовки `Accept*`) и условные запросы, позволяющие сэкономить трафик на возврате ответа целиком или частично (через заголовки `If-*`-заголовки, такие как `If-Range`, `If-Modified-Since` и так далее).
|
||||
|
||||
@ -84,7 +84,7 @@ URL принято раскладывать на составляющие, ка
|
||||
|
||||
Важнейшая составляющая HTTP запроса — это глагол (метод), описывающий операцию, применяемую к ресурсу. RFC 9110 стандартизирует восемь глаголов — `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `CONNECT`, `OPTIONS` и `TRACE` — из которых нас как разработчиков API интересует первые четыре. `CONNECT`, `OPTIONS` и `TRACE` — технические методы, которые очень редко используются в HTTP API (за исключением `OPTIONS`, который необходимо реализовать, если необходим доступ к API из браузера). Теоретически, `HEAD` (метод получения *только метаданных*, то есть заголовков, ресурса) мог бы быть весьма полезен в HTTP API, но по неизвестным нам причинам практически в этом смысле не используется.
|
||||
|
||||
Помимо RFC 9110, множество других RFC предлагают использовать дополнительные HTTP-глаголы (такие, например, как `COPY`, `LOCK`, `SEARCH` — полный список можно найти [здесь](http://www.iana.org/assignments/http-methods/http-methods.xhtml)), однако из всего разнообразия предложенных стандартов лишь один имеет широкое хождение — метод `PATCH`. Причины такого положения дел довольно тривиальны — этих пяти методов (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) достаточно для почти любого HTTP API.
|
||||
Помимо RFC 9110, множество других RFC предлагают использовать дополнительные HTTP-глаголы (такие, например, как `COPY`, `LOCK`, `SEARCH` — полный список можно найти в реестре[ref Hypertext Transfer Protocol (HTTP) Method Registry](https://www.iana.org/assignments/http-methods/http-methods.xhtml)), однако из всего разнообразия предложенных стандартов лишь один имеет широкое хождение — метод `PATCH`. Причины такого положения дел довольно тривиальны — этих пяти методов (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) достаточно для почти любого HTTP API.
|
||||
|
||||
HTTP-глагол определяет два важных свойства HTTP-вызова:
|
||||
* его семантику (что представляет собой операция);
|
||||
@ -168,7 +168,7 @@ HTTP-глагол определяет два важных свойства HTTP
|
||||
* при этом со *значениями* заголовков и вовсе неразбериха: часть из них по стандарту обязательно нечувствительна к регистру (в частности, `Content-Type`), а часть, напротив, обязательно чувствительна (например, `ETag`);
|
||||
* наборы допустимых символов и правила экранирования также различны для разных частей запроса
|
||||
* для path, например, стандарта экранирования символов `/`, `?` и `#` не существует;
|
||||
* символы unicode могут использоваться в доменных именах (хотя эта функциональность не везде поддерживается) только через своеобразную технику кодирования под названием [Punycode](https://www.rfc-editor.org/rfc/rfc3492.txt);
|
||||
* символы unicode могут использоваться в доменных именах (хотя эта функциональность не везде поддерживается) только через своеобразную технику кодирования под названием Punycode[ref Punycode: A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)](https://www.rfc-editor.org/rfc/rfc3492.txt);
|
||||
* для разных частей запросов используется разный кейсинг:
|
||||
* `kebab-case` для домена, заголовков и пути;
|
||||
* `snake_case` для query-параметров;
|
||||
|
@ -47,7 +47,7 @@ HTTP/1.1 200 OK
|
||||
* сервисы B и C обратятся к сервису A, проверят токен (переданный через проксирование заголовка `Authorization` или как явный параметр запроса), и вернут данные по запросу — профиль пользователя и список его заказов;
|
||||
* сервис D скомбинирует ответы сервисов B и C и вернёт их клиенту.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
|
||||
Нетрудно заметить, что мы тем самым создаём излишнюю нагрузку на сервис A: теперь к нему обращается каждый из вложенных микросервисов; даже если мы откажемся от аутентификации пользователей в конечных сервисах, оставив её только в сервисе D, проблему это не решит, поскольку сервисы B и C самостоятельно выяснить идентификатор пользователя не могут. Очевидный способ избавиться от лишних запросов — сделать так, чтобы однажды полученный `user_id` передавался остальным сервисам по цепочке:
|
||||
* гейтвей D получает запрос и через сервис A меняет токен на `user_id`
|
||||
@ -60,7 +60,7 @@ HTTP/1.1 200 OK
|
||||
GET /v1/orders?user_id=<user id>
|
||||
```
|
||||
|
||||
[]()
|
||||
[]()
|
||||
|
||||
**NB**: мы использовали нотацию `/v1/orders?user_id`, а не, допустим, `/v1/users/{user_id}/orders` по двум причинам:
|
||||
* сервис текущих заказов хранит заказы, а не пользователей — логично если URL будет это отражать;
|
||||
@ -75,7 +75,7 @@ HTTP/1.1 200 OK
|
||||
* такие «внешние» данные являются лишь идентификаторами контекстов, и сам микросервис никак их не трактует;
|
||||
* если всё же какие-то дополнительные операции с внешними данными требуется производить (например, проверять, авторизована ли запрашивающая сторона на выполнение операции), то следует **организовать операцию так, чтобы свести её к проверке целостности переданных данных**.
|
||||
|
||||
В нашем примере мы могли бы избавиться от лишних запросов к сервису A иначе — начав использовать stateless-токены, например, по [стандарту JWT](https://www.rfc-editor.org/rfc/rfc7519). Тогда сервисы B и C смогут сами раскодировать токен и извлечь идентификатор пользователя.
|
||||
В нашем примере мы могли бы избавиться от лишних запросов к сервису A иначе — начав использовать stateless-токены, например, по стандарту JWT[ref JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519). Тогда сервисы B и C смогут сами раскодировать токен и извлечь идентификатор пользователя.
|
||||
|
||||
Пойдём теперь чуть дальше и подметим, что профиль пользователя меняется достаточно редко, и нет никакой нужды каждый раз получать его заново — мы могли бы организовать кэш профилей на стороне гейтвея D. Для этого нам нужно сформировать ключ кэша, которым фактически является идентификатор клиента. Мы можем пойти длинным путём:
|
||||
* перед обращением в сервис B составить ключ и обратиться к кэшу;
|
||||
@ -105,7 +105,7 @@ ETag: <ревизия>
|
||||
* если сервис C отвечает статусом `304 Not Modified`, вернуть данные из кэша;
|
||||
* если сервис C отвечает новой версией данных, сохранить её в кэш и вернуть обновленный результат клиенту.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
|
||||
Использовав такое решение [функциональность управления кэшом через `ETag` ресурсов], мы автоматически получаем ещё один приятный бонус: эти же данные пригодятся нам, если пользователь попытается создать новый заказ. Если мы используем оптимистичное управление параллелизмом, то клиент должен передать в запросе актуальную ревизию ресурса `orders`:
|
||||
|
||||
@ -133,7 +133,7 @@ ETag: <новая ревизия>
|
||||
|
||||
и обновить кэш в соответствии с новыми данными.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
|
||||
**Важно**: обратите внимание на то, что, после всех преобразований, мы получили систему, в которой мы можем *убрать гейтвей D* и возложить его функции непосредственно на клиентский код. В самом деле, ничто не мешает клиенту:
|
||||
* хранить на своей стороне `user_id` (либо извлекать его из токена, если формат позволяет) и последний полученный `ETag` состояния списка заказов;
|
||||
|
@ -41,7 +41,7 @@
|
||||
|
||||
Иными словами, для любых операций, результат которых представляет собой результат работы какого-то алгоритма (например, список релевантных предложений по запросу) мы всегда будем сталкиваться с выбором, что важнее: семантика глагола или отсутствие побочных эффектов? Кэширование ответа или индикация того, что операция вычисляет результаты на лету?
|
||||
|
||||
**NB**: эта дихотомия волнует не только нас, но и авторов стандарта, которые в конечном итоге [предложили новый глагол `QUERY`](https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html), который по сути является немодифицирующим `POST`. Мы, однако, сомневаемся, что он получит широкое распространение — поскольку [уже существующий `SEARCH`](https://www.rfc-editor.org/rfc/rfc5323) оказался в этом качестве никому не нужен.
|
||||
**NB**: эта дихотомия волнует не только нас, но и авторов стандарта, которые в конечном итоге предложили новый глагол `QUERY`[ref The HTTP QUERY Method](https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html), который по сути является немодифицирующим `POST`. Мы, однако, сомневаемся, что он получит широкое распространение — поскольку уже существующий `SEARCH`[ref Web Distributed Authoring and Versioning (WebDAV) SEARCH](https://www.rfc-editor.org/rfc/rfc5323) оказался в этом качестве никому не нужен.
|
||||
|
||||
Простых ответов на вопросы выше у нас, к сожалению, нет. В рамках настоящей книги мы придерживаемся следующего подхода: сигнатура вызова в первую очередь должна быть лаконична и читабельна. Усложнение сигнатур в угоду абстрактным концепциям нежелательно. Применительно к указанным проблемам это означает, что:
|
||||
1. Метаданные операции не должны менять смысл операции; если запрос доходит до конечного микросервиса вообще без заголовков, он всё ещё должен быть выполним, хотя какая-то вспомогательная функциональность может деградировать или отсутствовать.
|
||||
|
@ -59,7 +59,7 @@ If-Match: <ревизия>
|
||||
|
||||
Разработчики стандарта HTTP об этой проблеме вполне осведомлены, и отдельно отмечают, что для решения бизнес-сценариев необходимо передавать в метаданных либо теле ответа дополнительные данные для описания возникшей ситуации («the server SHOULD send a representation containing an explanation of the error situation, and whether it is a temporary or permanent condition»), что (как и введение новых специальных кодов ошибок) противоречит самой идее унифицированного машиночитаемого формата ошибок. (Отметим, что отсутствие стандартов описания ошибок в бизнес-логике — одна из основных причин, по которым мы считаем разработку REST API как его описал Филдинг в манифесте 2008 года невозможной; клиент *должен* обладать априорным знанием о том, как работать с метаинформацией об ошибке, иначе он сможет восстанавливать своё состояние после ошибки только перезагрузкой.)
|
||||
|
||||
**NB**: не так давно разработчики стандарта предложили собственную версию спецификации JSON-описания HTTP-ошибок — [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html). Вы можете воспользоваться ей, но имейте в виду, что она покрывает только самый базовый сценарий:
|
||||
**NB**: не так давно разработчики стандарта предложили собственную версию спецификации JSON-описания HTTP-ошибок — RFC 9457[ref RFC 9457 Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457.html). Вы можете воспользоваться ей, но имейте в виду, что она покрывает только самый базовый сценарий:
|
||||
* подтип ошибки не передаётся в мета-информации;
|
||||
* нет разделения на сообщение для пользователя и сообщение для разработчика;
|
||||
* конкретный машиночитаемый формат описания ошибок остаётся на усмотрение разработчика.
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
2. Включайте в ответы стандартные заголовки — `Date`, `Content-Type`, `Content-Encoding`, `Content-Length`, `Cache-Control`, `Retry-After` — и вообще старайтесь не полагаться на то, что клиент правильно догадывается о параметрах протокола по умолчанию.
|
||||
|
||||
3. Поддержите метод `OPTIONS` и [протокол CORS](https://fetch.spec.whatwg.org/#cors-protocol) на случай, если ваш API захотят использовать из браузеров.
|
||||
3. Поддержите метод `OPTIONS` и протокол CORS[ref Fetch Living Standard. CORS protocol](https://fetch.spec.whatwg.org/#http-cors-protocol) на случай, если ваш API захотят использовать из браузеров.
|
||||
|
||||
4. Определитесь с правилами выбора кейсинга параметров (и преобразований кейсинга при перемещении параметра между различными частями запроса) и придерживайтесь их.
|
||||
|
||||
@ -31,12 +31,11 @@
|
||||
* не размещайте в пути и домене URL параметры, по формату требующие эскейпинга (т.е. могущие содержать символы, отличные от цифр и букв латинского алфавита); для этой цели лучше воспользоваться query-параметрами или телом запроса.
|
||||
|
||||
8. Ознакомьтесь хотя бы с основными видами уязвимостей в типичных имплементациях HTTP API, которыми могут воспользоваться злоумышленники:
|
||||
* [CSFR](https://owasp.org/www-community/attacks/csrf)
|
||||
* [SSRF](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery)
|
||||
* [HTTP Response Splitting](https://owasp.org/www-community/attacks/HTTP_Response_Splitting)
|
||||
* [Unvalidated Redirects and Forwards](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html)
|
||||
|
||||
и заложите защиту от этих векторов атак на уровне вашего серверного ПО. Организация OWASP предоставляет [хороший обзор лучших security-практик для HTTP API](https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html).
|
||||
* CSRF[ref Cross Site Request Forgery (CSRF)](https://owasp.org/www-community/attacks/csrf)
|
||||
* SSRF[ref Server Side Request Forgery](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery)
|
||||
* HTTP Response Splitting[ref HTTP Response Splitting](https://owasp.org/www-community/attacks/HTTP_Response_Splitting)
|
||||
* Unvalidated Redirects and Forwards[ref Unvalidated Redirects and Forwards Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html)
|
||||
|
||||
и заложите защиту от этих векторов атак на уровне вашего серверного ПО. Организация OWASP предоставляет хороший обзор лучших security-практик для HTTP API[ref REST Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html).
|
||||
|
||||
В заключение хотелось бы сказать следующее: HTTP API — это способ организовать ваше API так, чтобы полагаться на понимание семантики операций как разнообразным программным обеспечением, от клиентских фреймворков до серверных гейтвеев, так и разработчиком, который читает спецификацию. В этом смысле экосистема HTTP предоставляет пожалуй что наиболее широкий (и в плане глубины, и в плане распространённости) по сравнению с другими технологиями словарь для описания самых разнообразных ситуаций, возникающих во время работы клиент-серверных приложений. Разумеется, эта технология не лишена своих недостатков, но для разработчика *публичного* API она является выбором по умолчанию — на сегодняшний день скорее надо обосновывать отказ от HTTP API чем выбор в его пользу.
|
@ -11,24 +11,32 @@
|
||||
|
||||
[]()
|
||||
|
||||
2. Комбинирование краткого и полного описания предложения в одном интерфейсе (предложение можно развернуть прямо в списке и сразу сделать заказ)
|
||||
* иллюстрирует проблему полного удаления одного из субкомпонентов при сохранении бизнес-логики и UX:
|
||||
2. Комбинирование краткого и полного описания предложения в одном интерфейсе (предложение можно развернуть прямо в списке и сразу сделать заказ):
|
||||
* иллюстрирует проблему полного удаления одного из субкомпонентов с передачей его бизнес-логики другим частям системы;
|
||||
|
||||
[]()
|
||||
|
||||
[]()
|
||||
|
||||
3. Манипуляция данными и доступными действиями для предложения через добавление новых кнопок (вперёд, назад, позвонить) и управление их содержимым. Отметим, что каждая из кнопок предоставляет свою проблему с точки зрения имплементации:
|
||||
* кнопки навигации (вперёд/назад) требуют, чтобы информация о связности списка (есть предыдущий / следующий заказ) каким-то образом дошла до панели показа предложения;
|
||||
* кнопка «позвонить» показывается динамически, если номер кофейни известен, и, таким образом, требует возможности определения списка показываемых кнопок динамически, отдельно для каждого конкретного предложения.
|
||||
|
||||
Пример иллюстрирует проблемы неоднозначности иерархий наследования и сильной связности компонентов.
|
||||
3. Манипуляция доступными действиями для предложения через добавление новых кнопок (вперёд, назад, позвонить) и управление их содержимым.
|
||||
|
||||
[]()
|
||||
|
||||
В этом сценарии мы рассматриваем различные цепочки пропагирования информации и настроек до панели предложения и динамическое построение UI на их основе:
|
||||
|
||||
Решение, которое напрашивается в данной ситуации — это выделение двух дополнительных компонентов, отвечающих за представление списка предложений и за панель показа конкретного предложения, назовём их OfferList и OfferPanel. В случае отсутствия требований кастомизации, псевдокод, имплементирующий взаимодействие всех трёх компонентов, выглядел бы достаточно просто:
|
||||
* часть данных является свойствами реального объекта (логотип, номер телефона), полученными из API поиска предложений;
|
||||
|
||||
```
|
||||
* часть данных имеет смысл только в рамках конкретного UI и отражает механику его построения (кнопки «Вперёд» и «Назад»);
|
||||
|
||||
* часть данных (иконки отмены и звонка) связаны с типом кнопки (бизнес-логикой, которую она несёт в себе).
|
||||
|
||||
Решение, которое напрашивается в данной ситуации — это выделение двух дополнительных компонентов, отвечающих за представление списка предложений и за панель показа конкретного предложения, назовём их `OfferList` и `OfferPanel`.
|
||||
|
||||
[]()
|
||||
|
||||
В случае отсутствия требований кастомизации, псевдокод, имплементирующий взаимодействие всех трёх компонентов, выглядел бы достаточно просто:
|
||||
|
||||
```typescript
|
||||
class SearchBox implements ISearchBox {
|
||||
// Ответственность `SearchBox`:
|
||||
// 1. Создать контейнер для визуального
|
||||
@ -73,7 +81,7 @@ class SearchBox implements ISearchBox {
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
```typescript
|
||||
class OfferList implements IOfferList {
|
||||
// Ответственность OfferList:
|
||||
// 1. Создать контейнер для визуального
|
||||
@ -104,7 +112,7 @@ class OfferList implements IOfferList {
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
```typescript
|
||||
class OfferPanel implements IOfferPanel {
|
||||
constructor(searchBox, container, options) { … }
|
||||
// Ответственность панели показа предложения:
|
||||
@ -129,7 +137,7 @@ class OfferPanel implements IOfferPanel {
|
||||
```
|
||||
|
||||
Интерфейсы `ISearchBox` / `IOfferPanel` / `IOfferView` также очень просты (конструкторы и деструкторы опущены):
|
||||
```
|
||||
```typescript
|
||||
interface ISearchBox {
|
||||
search(query);
|
||||
createOrder(offer);
|
||||
@ -145,7 +153,8 @@ interface IOfferPanel {
|
||||
Если бы мы не разрабатывали SDK и у нас не было бы задачи разрешать кастомизацию этих компонентов, подобная реализация была бы стопроцентно уместной. Попробуем, однако, представить, как мы будем решать описанные выше задачи:
|
||||
|
||||
1. Показ списка предложений на карте: на первый взгляд, мы можем разработать альтернативный компонент показа списка предложений, скажем, `OfferMap`, который сможет использовать стандартную панель предложений. Но у нас есть одна проблема: если `OfferList` только отправляет команды для `OfferPanel`, то `OfferMap` должен ещё и получать обратную связь — событие закрытия панели, чтобы убрать выделение с метки. Наш интерфейс подобного не предусматривает. Имплементация этой функциональности не так и проста:
|
||||
```
|
||||
|
||||
```typescript
|
||||
class CustomOfferPanel extends OfferPanel {
|
||||
constructor(
|
||||
searchBox, offerMap, container, options
|
||||
@ -154,7 +163,7 @@ interface IOfferPanel {
|
||||
super(searchBox, container, options);
|
||||
}
|
||||
onCancelButtonClick() {
|
||||
offerMap.resetCurrentOffer();
|
||||
/* <em> */offerMap.resetCurrentOffer();/* </em> */
|
||||
super.onCancelButtonClick();
|
||||
}
|
||||
}
|
||||
@ -173,15 +182,16 @@ interface IOfferPanel {
|
||||
}
|
||||
```
|
||||
|
||||
Нам пришлось создать новый класс CustomOfferPanel, который, в отличие от своего родителя, теперь работает только со специфической имплементацией интерфейса IOfferList.
|
||||
Нам пришлось создать новый класс `CustomOfferPanel`, который, в отличие от своего родителя, теперь работает не с любой имплементацией интерфейса IOfferList, а только с `IOfferMap`.
|
||||
|
||||
2. Полные описания и заказ в самом списке заказов — в этом случае всё достаточно очевидно: мы можем добиться нужной функциональности только созданием собственного компонента. Даже если мы предоставим метод переопределения внешнего вида элемента списка для стандартного компонента `OfferList`, он всё равно продолжит создавать `OfferPanel` и открывать его по выбору предложения.
|
||||
2. Полные описания и кнопки действий в самом списке заказов — в этом случае всё достаточно очевидно: мы можем добиться нужной функциональности только созданием собственного компонента. Даже если мы предоставим метод переопределения внешнего вида элемента списка для стандартного компонента `OfferList`, он всё равно продолжит создавать `OfferPanel` и открывать его по выбору предложения.
|
||||
|
||||
3. Для реализации новых кнопок мы можем только лишь предложить программисту реализовать свой список предложений (чтобы предоставить методы выбора предыдущего / следующего предложения) и свою панель предложений, которая эти методы будет вызывать. Даже если мы задаимся какой нибудь простой кастомизацией, например, текста кнопки «Сделать заказ», то в данном коде она фактически является ответственностью класса `OfferList`:
|
||||
```
|
||||
3. Для реализации новых кнопок мы можем только лишь предложить программисту реализовать свой список предложений (чтобы предоставить методы выбора предыдущего / следующего предложения) и свою панель предложений, которая эти методы будет вызывать. Даже если мы придумаем какой-нибудь простой способ кастомизировать, например, текст кнопки «Сделать заказ», его поддержка всё равно будет ответственностью компонента `OfferList`:
|
||||
|
||||
```typescript
|
||||
const searchBox = new SearchBox(…, {
|
||||
offerPanelCreateOrderButtonText:
|
||||
'Drink overpriced coffee!'
|
||||
/* <em> */offerPanelCreateOrderButtonText:
|
||||
'Drink overpriced coffee!'/* </em> */
|
||||
});
|
||||
|
||||
class OfferList {
|
||||
@ -192,26 +202,26 @@ interface IOfferPanel {
|
||||
// вынужен конструктор класса
|
||||
// `OfferList`
|
||||
this.offerPanel = new OfferPanel(…, {
|
||||
createOrderButtonText: options
|
||||
.offerPanelCreateOrderButtonText
|
||||
/* <em> */createOrderButtonText: options
|
||||
.offerPanelCreateOrderButtonText/* </em> */
|
||||
…
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Самая же неприятная особенность кода из п. 1 — его очень плохая расширяемость. Допустим, мы решили сделать функциональность реакции `OfferList` на закрытие панели предложений частью интерфейса, чтобы программист мог ей воспользоваться. Для этого нам придётся объявить новый необязательный метод:
|
||||
Неприятная особенность всех вышеперечисленных решений — их очень плохая расширяемость. Вернёмся к п.1: допустим, мы решили сделать функциональность реакции списка предложений на закрытие панели предложений частью интерфейса, чтобы программист мог ей воспользоваться. Для этого нам придётся объявить новый метод, который в целях обратной совместимости будет необязательным:
|
||||
|
||||
```
|
||||
```typescript
|
||||
interface IOfferList {
|
||||
…
|
||||
onOfferPanelClose?();
|
||||
}
|
||||
```
|
||||
|
||||
и писать в коде OfferPanel что-то типа:
|
||||
и писать в коде `OfferPanel` что-то типа:
|
||||
|
||||
```
|
||||
```typescript
|
||||
if (Type(this.offerList.onOfferPanelClose)
|
||||
== 'function') {
|
||||
this.offerList.onOfferPanelClose();
|
||||
@ -220,11 +230,12 @@ if (Type(this.offerList.onOfferPanelClose)
|
||||
|
||||
Что, во-первых, совершенно не красит наш код и, во-вторых, делает связность `OfferPanel` и `OfferList` ещё более сильной.
|
||||
|
||||
Как мы описывали ранее в главе «[Слабая связность](#back-compat-weak-coupling)», избавиться от такого рода проблем мы можем, если перейдём от сильной связности к слабой, например, через генерацию событий вместо вызова методов:
|
||||
Как мы описывали ранее в главе «[Слабая связность](#back-compat-weak-coupling)», избавиться от такого рода проблем мы можем, если перейдём от сильной связности к слабой, например, через генерацию событий вместо вызова методов. Компонент `IOfferPanel` мог бы бросать событие `'close'`, и тогда `OfferList` мог бы на него подписаться:
|
||||
|
||||
```
|
||||
```typescript
|
||||
class OfferList {
|
||||
setup() {
|
||||
…
|
||||
this.offerPanel.events.on(
|
||||
'close',
|
||||
function () {
|
||||
@ -236,11 +247,11 @@ class OfferList {
|
||||
}
|
||||
```
|
||||
|
||||
Код выглядит более разумно написанным, но никак не уменьшает связность: использовать `OfferList` без `OfferPanel` мы всё ещё не можем.
|
||||
Код выглядит более разумно написанным, но никак не уменьшает связность: использовать `OfferList` без `OfferPanel`, как этого требует сценарий \#2, мы всё ещё не можем.
|
||||
|
||||
Во всех вышеприведённых фрагментах кода налицо полный хаос с уровнями абстракции: `OfferList` инстанцирует `OfferPanel` и управляет ей напрямую. При этом `OfferPanel` приходится перепрыгивать через уровни, чтобы создать заказ. Мы можем попытаться разомкнуть эту связь, если начнём маршрутизировать потоки команд через сам `SearchBox`, например, так:
|
||||
|
||||
```
|
||||
```typescript
|
||||
class SearchBox() {
|
||||
constructor() {
|
||||
this.offerList = new OfferList(…);
|
||||
@ -262,7 +273,7 @@ class SearchBox() {
|
||||
|
||||
Теперь `OfferList` и `OfferPanel` стали независимы друг от друга, но мы получили другую проблему: для их замены на альтернативные имплементации нам придётся переписать сам `SearchBox`. Мы можем абстрагироваться ещё дальше, поступив вот так:
|
||||
|
||||
```
|
||||
```typescript
|
||||
class SearchBox() {
|
||||
constructor() {
|
||||
…
|
||||
@ -279,7 +290,7 @@ class SearchBox() {
|
||||
|
||||
То есть заставить `SearchBox` транслировать события, возможно, с преобразованием данных. Мы даже можем заставить `SearchBox` транслировать *любые* события дочерних компонентов, и, таким образом, прозрачным образом расширять функциональность, добавляя новые события. Но это совершенно очевидно не ответственность высокоуровневого компонента — состоять, в основном, из кода трансляции событий. К тому же, в этих цепочках событий очень легко запутаться. Как, например, должна быть реализована функциональность выбора следующего предложения в `offerPanel` (п. 3 в нашем списке улучшений)? Для этого необходимо, чтобы `OfferList` не только генерировал сам событие `offerSelect`, но и прослушивал это событие на родительском контексте и реагировал на него. В этом коде легко можно организовать бесконечный цикл:
|
||||
|
||||
```
|
||||
```typescript
|
||||
class OfferList {
|
||||
constructor(searchBox, …) {
|
||||
…
|
||||
@ -314,7 +325,7 @@ class SearchBox {
|
||||
|
||||
Во избежание таких циклов мы можем разделить события:
|
||||
|
||||
```
|
||||
```typescript
|
||||
class SearchBox {
|
||||
constructor() {
|
||||
…
|
||||
@ -402,7 +413,7 @@ class SearchBoxComposer implements ISearchBoxComposer {
|
||||
Мы можем придать `SearchBoxComposer`-у функциональность трансляции любых контекстов. В частности:
|
||||
|
||||
1. Трансляцию данных и подготовку данных. На этом уровне мы можем предположить, что `offerList` показывает краткую информацию о предложений, а `offerPanel` — полную, и предоставить (потенциально переопределяемые) методы генерации нужных срезов данных:
|
||||
```
|
||||
```typescript
|
||||
class SearchBoxComposer {
|
||||
…
|
||||
onContextOfferListChange(offerList) {
|
||||
@ -422,7 +433,7 @@ class SearchBoxComposer implements ISearchBoxComposer {
|
||||
}
|
||||
```
|
||||
2. Логику управления собственным состоянием (в нашем случае полем `currentOffer`):
|
||||
```
|
||||
```typescript
|
||||
class SearchBoxComposer {
|
||||
…
|
||||
onContextOfferListChange(offerList) {
|
||||
@ -443,12 +454,12 @@ class SearchBoxComposer implements ISearchBoxComposer {
|
||||
}
|
||||
```
|
||||
3. Логику преобразования действий пользователя на одном из субкомпонентов в события или действия над другими компонентами или родительским контекстом:
|
||||
```
|
||||
```typescript
|
||||
class SearchBoxComposer {
|
||||
…
|
||||
public performAction({
|
||||
action, offerId
|
||||
} {
|
||||
}) {
|
||||
switch (action) {
|
||||
case 'createOrder':
|
||||
// Действие «создать заказ»
|
||||
@ -480,4 +491,4 @@ class SearchBoxComposer implements ISearchBoxComposer {
|
||||
Пример реализации компонентов с описанными интерфейсами и имплементацией всех трёх кейсов вы можете найти в репозитории настоящей книги:
|
||||
* исходный код доступен на [www.github.com/twirl/The-API-Book/docs/examples](https://github.com/twirl/The-API-Book/tree/gh-pages/docs/examples/01.%20Decomposing%20UI%20Components)
|
||||
* там же предложены несколько задач для самостоятельного изучения;
|
||||
* песочница с «живыми» примерами достпна на [twirl.github.io/The-API-Book](https://twirl.github.io/The-API-Book/examples/01.%20Decomposing%20UI%20Components/).
|
||||
* песочница с «живыми» примерами доступна на [twirl.github.io/The-API-Book](https://twirl.github.io/The-API-Book/examples/01.%20Decomposing%20UI%20Components/).
|
@ -21,7 +21,7 @@
|
||||
2. Базовый уровень — работы с продуктовыми сущностями через формальные интерфейсы. [В случае нашего учебного API этому уровню соответствует HTTP API заказа.]
|
||||
3. Упростить работу с продуктовыми сущностями можно, предоставив SDK для различных платформ, скрывающие под собой сложности работы с формальными интерфейсами и адаптирующие концепции API под соответствующие парадигмы (что позволяет разработчикам, знакомым только с конкретной платформой, не тратить время и не разбираться в формальных интерфейсах и протоколах).
|
||||
4. Ещё более упростить работу можно с помощью сервисов, генерирующих код. В таком интерфейсе разработчик выбирает один из представленных шаблонов интеграции, кастомизирует некоторые параметры, и получает на выходе готовый фрагмент кода, который он может вставить в своё приложение (и, возможно, дописать необходимую функциональность с использованием API 1-3 уровней). Подобного рода подход ещё часто называют «программированием мышкой». [В случае нашего кофейного API примером такого сервиса мог бы служить визуальный редактор форм/экранов, в котором пользователь расставляет UI элементы и получает полный код приложения, или консольный скрипт, который генерирует «скелет» приложения.]
|
||||
5. Ещё более упростить такой подход можно, если результатом работы такого сервиса будет уже не код поверх API, а готовый компонент / виджет / фрейм, подключаемый одной строкой. [Например, если мы дадим возможность разработчику вставлять на свой сайт iframe, в котором можно заказать кофе, кастомизированный под нужды заказчика, либо, ещё проще, описать правила формирования «[deep link](https://en.wikipedia.org/wiki/Mobile_deep_linking)-а», который приведёт пользователя на наш сервис.]
|
||||
5. Ещё более упростить такой подход можно, если результатом работы такого сервиса будет уже не код поверх API, а готовый компонент / виджет / фрейм, подключаемый одной строкой. [Например, если мы дадим возможность разработчику вставлять на свой сайт iframe, в котором можно заказать кофе, кастомизированный под нужды заказчика, либо, ещё проще, описать правила формирования «deep link-а»[ref Mobile Deep Linking](https://en.wikipedia.org/wiki/Mobile_deep_linking), который приведёт пользователя на наш сервис.]
|
||||
|
||||
В конечном итоге можно прийти к концепции мета-API, когда готовые визуальные компоненты тоже будут иметь какое-то свой высокоуровневый API, который «под капотом» будет обращаться к базовым API.
|
||||
|
||||
|
@ -6,8 +6,10 @@
|
||||
"description": "Разработка API — особый навык: API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок. Эта книга написана для того, чтобы поделиться опытом и изложить лучшие практики разработки API. Книга состоит из шести разделов, посвящённых проектированию API, паттернам дизайна API, поддержанию обратной совместимости, HTTP API и архитектурным принципам REST, SDK и UI-библиотекам, продуктовому управлению API.",
|
||||
"publisher": "Сергей Константинов",
|
||||
"copyright": "© Сергей Константинов, 2023",
|
||||
"isbn": "ISBN",
|
||||
"locale": "ru_RU",
|
||||
"file": "API",
|
||||
"references": "Примечания",
|
||||
"landingFile": "index.ru.html",
|
||||
"aboutMe": {
|
||||
"title": "Об авторе",
|
||||
@ -16,9 +18,8 @@
|
||||
"<p>За это время Сергей получил уникальный опыт построения API мирового уровня с дневной аудиторией в десятки миллионов человек, планирования роадмапов для такого продукта и многочисленных публичных выступлений. Он также проработал полтора года в составе Технической архитектурной группы W3C.</p>",
|
||||
"<p>После девяти лет в Картах Сергей переключился на технические роли в других департаментах и компаниях, занимаясь интеграционными проектами и будучи ответственным за техническую архитектуру целых продуктов компании. Сегодня Сергей живёт в Таллинне, Эстония, и работает ведущим инженером в компании Bolt.</p>"
|
||||
],
|
||||
"imageCredit": "Фото: <a href=\"http://linkedin.com/in/zloylos/\">Denis Hananein</a>"
|
||||
"imageCredit": "Фото: <a href=\"https://linkedin.com/in/zloylos/\">Denis Hananein</a>"
|
||||
},
|
||||
"ctl": "Нажмите для увеличения",
|
||||
"url": "https://twirl.github.io/The-API-Book/index.ru.html",
|
||||
"favicon": "/img/favicon.png",
|
||||
"sidePanel": {
|
||||
@ -45,7 +46,7 @@
|
||||
},
|
||||
{
|
||||
"key": "reddit",
|
||||
"link": "http://www.reddit.com/submit?url=${url}&title=${text}"
|
||||
"link": "https://www.reddit.com/submit?url=${url}&title=${text}"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -86,7 +87,7 @@
|
||||
"<li>продуктовому управлению API.</li></ul>",
|
||||
"<p class=\"text-align-left\">Иллюстрации и вдохновение: Maria Konstantinova · <a href=\"https://www.instagram.com/art.mari.ka/\">art.mari.ka</a>.</p>",
|
||||
"<img class=\"cc-by-nc-img\" alt=\"Creative Commons «Attribution-NonCommercial» Logo\" src=\"https://i.creativecommons.org/l/by-nc/4.0/88x31.png\"/>",
|
||||
"<p class=\"cc-by-nc\">Это произведение доступно по <a href=\"http://creativecommons.org/licenses/by-nc/4.0/\">лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция — Некоммерческое использование») 4.0 Всемирная</a>.</p>"
|
||||
"<p class=\"cc-by-nc\">Это произведение доступно по <a href=\"https://creativecommons.org/licenses/by-nc/4.0/\">лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция — Некоммерческое использование») 4.0 Всемирная</a>.</p>"
|
||||
]
|
||||
},
|
||||
"landing": {
|
||||
@ -112,7 +113,7 @@
|
||||
"or": "или",
|
||||
"readOnline": "прочитать её онлайн",
|
||||
"liveExamples": "Интерактивные примеры",
|
||||
"license": "Это произведение доступно по <a href=\"http://creativecommons.org/licenses/by-nc/4.0/\">лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция — Некоммерческое использование») 4.0 Всемирная</a>.",
|
||||
"license": "Это произведение доступно по <a href=\"https://creativecommons.org/licenses/by-nc/4.0/\">лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция — Некоммерческое использование») 4.0 Всемирная</a>.",
|
||||
"footer": [
|
||||
"<p>You might also <a href=\"index.html\">read ‘The API’ in English</a>.</p>"
|
||||
]
|
||||
|
@ -23,6 +23,13 @@ module.exports = {
|
||||
mainScript: () =>
|
||||
`<script>${readFileSync(resolve('./src/scripts/sidebar.js'))}</script>`,
|
||||
|
||||
referenceIsbnHref: (href, text, l10n) => {
|
||||
const isbn = escapeHtml(href.replace('isbn:', ''));
|
||||
return `<a target="_blank" class="external${
|
||||
text ? '' : ' text'
|
||||
}" href="https://isbnsearch.org/isbn/${isbn}">${l10n.isbn} ${isbn}</a>`;
|
||||
},
|
||||
|
||||
sidePanel: ({
|
||||
structure,
|
||||
l10n,
|
||||
@ -136,6 +143,7 @@ module.exports = {
|
||||
property="og:url"
|
||||
content="${l10n.links.githubHref}"
|
||||
/>
|
||||
<link rel="stylesheet" href="assets/fonts.css"/>
|
||||
<link rel="stylesheet" href="assets/landing.css"/>
|
||||
</head>
|
||||
<body>
|
||||
@ -242,35 +250,26 @@ module.exports = {
|
||||
</html>`;
|
||||
},
|
||||
aImg: ({ src, href, title, alt, l10n, className = 'img-wrapper' }) => {
|
||||
const withCredit = alt != 'APP' && alt != 'PLOT';
|
||||
const fullTitle = escapeHtml(
|
||||
`${title}${title.at(-1).match(/[\.\?\!\)]/) ? ' ' : '. '}${
|
||||
alt != 'APP'
|
||||
? ` ${
|
||||
alt == 'CTL'
|
||||
? l10n.ctl
|
||||
: `${l10n.imageCredit}: ${alt}`
|
||||
}`
|
||||
: ''
|
||||
withCredit ? ` ${l10n.imageCredit}: ${alt}` : ''
|
||||
}`
|
||||
);
|
||||
const fullClass =
|
||||
alt == 'APP' ? `${className} app-img-wrapper` : className;
|
||||
return `<div class="${escapeHtml(
|
||||
fullClass
|
||||
)}"><a href="${src}" target="_blank"><img src="${escapeHtml(
|
||||
return `<div class="${escapeHtml(fullClass)}"><img src="${escapeHtml(
|
||||
src
|
||||
)}" alt="${fullTitle}" title="${fullTitle}"/></a><h6>${escapeHtml(
|
||||
title
|
||||
)}. ${
|
||||
alt == 'CTL' || alt == 'APP'
|
||||
? l10n.ctl
|
||||
: `${escapeHtml(l10n.imageCredit)}: ${
|
||||
)}" alt="${fullTitle}" title="${fullTitle}"/><h6>${escapeHtml(title)}${
|
||||
withCredit
|
||||
? `. ${escapeHtml(l10n.imageCredit)}: ${
|
||||
href
|
||||
? `<a href="${escapeHtml(href)}">${escapeHtml(
|
||||
alt
|
||||
)}</a>`
|
||||
: escapeHtml(alt)
|
||||
}`
|
||||
: ''
|
||||
}
|
||||
</h6></div>`;
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user