diff --git a/build.js b/build.js index fcb275f..ef57a4e 100644 --- a/build.js +++ b/build.js @@ -1,6 +1,7 @@ const fs = require('fs'); const path = require('path'); +const templates = require('./src/templates'); const builders = require('./src/lib/builders'); const mdHtml = require('./src/lib/md-html'); const htmlProcess = require('./src/lib/html-process'); @@ -8,24 +9,8 @@ const htmlProcess = require('./src/lib/html-process'); const css = fs.readFileSync('./src/style.css', 'utf-8'); const l10n = { - en: { - title: 'The API', - author: 'Sergey Konstantinov', - chapter: 'Chapter', - toc: 'Table of Contents', - frontPage: 'Front Page', - description: "Designing APIs is a very special skill: API is a multiplier to both your opportunities and mistakes. This book is written to share the expertise and describe the best practices in the API design. The book comprises three large sections. In Section I we'll discuss designing APIs as a concept: how to build the architecture properly, from a high-level planning down to final interfaces. Section II is dedicated to an API's lifecycle: how interfaces evolve over time, and how to elaborate the product to match users' needs. Finally, Section III is more about un-engineering sides of the API, like API marketing, organizing support, and working with a community.", - locale: 'en_US' - }, - ru: { - title: 'API', - author: 'Сергей Константинов', - chapter: 'Глава', - toc: 'Содержание', - frontPage: 'Титульный лист', - description: 'Разработка API — особый навык: API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок. Эта книга написана для того, чтобы поделиться опытом и изложить лучшие практики проектирования API. Книга состоит из трёх больших разделов. В первом разделе мы поговорим о проектировании API на стадии разработки концепции — как грамотно выстроить архитектуру, от крупноблочного планирования до конечных интерфейсов. Второй раздел будет посвящён жизненному циклу API — как интерфейсы эволюционируют со временем и как развивать продукт так, чтобы отвечать потребностям пользователей. Наконец, третий раздел будет касаться больше не-разработческих сторон жизни API — поддержки, маркетинга, работы с комьюнити.', - locale: 'ru_RU' - } + en: require('./src/en/l10n.json'), + ru: require('./src/ru/l10n.json') }; const langsToBuild = process.argv[2] && @@ -61,54 +46,31 @@ function buildDocs (langsToBuild, targets, l10n) { } async function buildDoc (lang, targets, l10n) { - const pageBreak = '
'; + const pageBreak = templates.pageBreak; const structure = await getStructure({ path: `./src/${lang}/clean-copy/`, l10n, pageBreak }); - const tableOfContents = `${pageBreak}`; - const getRef = (anchor) => { - return ``; - } + const tableOfContents = templates.toc(structure, l10n); const htmlContent = [ structure.frontPage, tableOfContents, ...structure.sections .map((section) => section.chapters.reduce((content, chapter) => { if (chapter.title) { - content.push(`
@@ -238,15 +251,13 @@ a.anchor:after {
The book you're holding in your hands comprises this Introduction and three large sections.
In Section I we'll discuss designing APIs as a concept: how to build the architecture properly, from a high-level planning down to final interfaces.
Section II is dedicated to an API's lifecycle: how interfaces evolve over time, and how to elaborate the product to match users' needs.
Finally, Section III is more about un-engineering sides of the API, like API marketing, organizing support, and working with a community.
First two sections are interesting to engineers mostly, while the third section is more relevant to both engineers and product managers. However, we insist that the third section is the most important for the API software developer. Since an API is a product for engineers, you cannot simply pronounce non-engineering team responsible for product planning and support. Nobody but you understands more about your API's product features.
-Let's start.
Let's start.
Before we start talking about the API design, we need to explicitly define what the API is. Encyclopedia tells us that ‘API’ is an acronym for ‘Application Program Interface’. This definition is fine, but useless. Much like ‘Man’ definition by Plato: Man stood upright on two legs without feathers. This definition is fine again, but it gives us no understanding what's so important about a Man. (Actually, not ‘fine’ either. Diogenes of Sinope once brought a plucked chicken, saying ‘That's Plato's Man’. And Plato had to add ‘with broad nails’ to his definition.)
What API means apart from the formal definition?
You're possibly reading this book using a Web browser. To make the browser display this page correctly, a bunch of stuff must work correctly: parsing the URL according to the specification; DNS service; TLS handshake protocol; transmitting the data over HTTP protocol; HTML document parsing; CSS document parsing; correct HTML+CSS rendering.
@@ -262,7 +273,7 @@ a.anchor:after {What differs between a Roman aqueduct and a good API is that APIs presume a contract being programmable. To connect two areas some coding is needed. The goal of this book is to help you in designing APIs which serve their purposes as solidly as a Roman aqueduct does.
An aqueduct also illustrates another problem of the API design: your customers are engineers themselves. You are not supplying water to end-users: suppliers are plugging their pipes to you engineering structure, building their own structures upon it. From one side, you may provide an access to the water to much more people through them, not spending your time on plugging each individual house to your network. But from other side, you can't control the quality of suppliers' solutions, and you are to be blamed every time there is a water problem caused by their incompetence.
-That's why designing the API implies a larger area of responsibility. API is a multiplier to both your opportunities and mistakes.
That's why designing the API implies a larger area of responsibility. API is a multiplier to both your opportunities and mistakes.
Before we start laying out the recommendations, we ought to specify what API we consider ‘fine’, and what's the profit of having a ‘fine’ API.
Let's discuss second question first. Obviously, API ‘finesse’ is first of all defined through its capability to solve developers' problems. (One may reasonably say that solving developers' problem might not be the main purpose of offering the API of ours to developers. However, manipulating public opinion is out of this book's author interest. Here we assume that APIs exist primarily to help developers in solving their problems, not for some other covertly declared purposes.)
So, how the API design might help the developers? Quite simple: a well-designed API must solve their problems in the most efficient and comprehensible manner. The distance from formulating the task to writing a working code must be as short as possible. Among other things, it means that:
@@ -273,13 +284,13 @@ a.anchor:after {However static convenience and clarity of APIs is a simple part. After all, nobody seeks for making an API deliberately irrational and unreadable. When we are developing an API, we always start with clear basic concepts. Providing you've got some experience in APIs, it's quite hard to make an API core which fails to meet obviousness, readability, and consistency criteria.
Problems begin when we start to expand our API. Adding new functionality sooner or later result in transforming once plain and simple API into a mess of conflicting concepts, and our efforts to maintain backwards compatibility lead to illogical, unobvious and simply bad design solutions. It is partly related to an inability to predict the future in details: your understanding of ‘fine’ APIs will change over time, both in objective terms (what problems the API is to solve, and what are the best practices) and in subjective ones too (what obviousness, readability and consistency really means regarding your API).
-Principles we are explaining below are specifically oriented to making APIs evolve smoothly over time, not being turned into a pile of mixed inconsistent interfaces. It is crucial to understand that this approach isn't free: a necessity to bear in mind all possible extension variants and to preserve essential growth points means interface redundancy and possibly excessing abstractions being embedded in the API design. Besides both make developers' work harder. Providing excess design complexities being reserved for future use makes sense only when this future actually exists for your API. Otherwise it's simply an overengineering.
Principles we are explaining below are specifically oriented to making APIs evolve smoothly over time, not being turned into a pile of mixed inconsistent interfaces. It is crucial to understand that this approach isn't free: a necessity to bear in mind all possible extension variants and to preserve essential growth points means interface redundancy and possibly excessing abstractions being embedded in the API design. Besides both make developers' work harder. Providing excess design complexities being reserved for future use makes sense only when this future actually exists for your API. Otherwise it's simply an overengineering.
Backwards compatibility is a temporal characteristics of your API. An obligation to maintain backwards compatibility is the crucial point where API development differs form software development in general.
Of course, backwards compatibility isn't an absolute. In some subject areas shipping new backwards incompatible API versions is a routine. Nevertheless, every time you deploy new backwards incompatible API version, the developers need to make some non-zero effort to adapt their code to the new API version. In this sense, releasing new API versions puts a sort of a ‘tax’ on customers. They must spend quite real money just to make sure their product continue working.
Large companies, which occupy firm market positions, could afford implying such a taxation. Furthermore, they may introduce penalties for those who refuse to adapt their code to new API versions, up to disabling their applications.
From our point of view such practice cannot be justified. Don't imply hidden taxes on your customers. If you're able to avoid breaking backwards compatibility — never break it.
Of course, maintaining old API versions is a sort of a tax either. Technology changes, and you cannot foresee everything, regardless of how nice your API is initially designed. At some point keeping old API versions results in an inability to provide new functionality and support new platforms, and you will be forced to release new version. But at least you will be able to explain to your customers why they need to make an effort.
-We will discuss API lifecycle and version policies in Section II.
We will discuss API lifecycle and version policies in Section II.
Here and throughout we firmly stick to semver principles of versioning:
1.2.3
.Sentences ‘major API version’ and ‘new API version, containing backwards incompatible changes’ are therefore to be considered as equivalent ones.
-In Section II we will discuss versioning policies in more details. In Section I we will just use semver versions designation, specifically v1
, v2
, etc.
In Section II we will discuss versioning policies in more details. In Section I we will just use semver versions designation, specifically v1
, v2
, etc.
Software development is being characterized, among other things, by an existence of many different engineering paradigms, whose adepts sometimes are quite aggressive towards other paradigms' adepts. While writing this book we are deliberately avoiding using terms like ‘method’, ‘object’, ‘function’, and so on, using a neutral term ‘entity’ instead. ‘Entity’ means some atomic functionality unit, like class, method, object, monad, prototype (underline what you think right).
As for an entity's components, we regretfully failed to find a proper term, so we will use words ‘fields’ and ‘methods’.
Most of the examples of APIs will be provided in a form of JSON-over-HTTP endpoints. This is some sort of notation which, as we see it, helps to describe concepts in the most comprehensible manner. GET /v1/orders
endpoint call could easily be replaced with orders.get()
method call, local or remote; JSON could easily be replaced with any other data format. The meaning of assertions shouldn't change.
Some request and response parts might be omitted if they are irrelevant to a topic being discussed.
Simplified notation might be used to avoid redundancies, like POST /some-resource
{…, "some_parameter", …}
→ { "operation_id" }
; request and response bodies might also be omitted.
We will be using sentences like ‘POST /v1/bucket/{id}/some-resource
method’ (or simply ‘bucket/some-resource
method’, ‘some-resource
’ method — if there are no other some-resource
s in the chapter, so there is no ambiguity) to refer to such endpoint definitions.
Apart from HTTP API notation, we will employ C-style pseudocode, or, to be more precise, JavaScript-like or Python-like since types are omitted. We assume such imperative structures being readable enough to skip detailed grammar explanations.
-Apart from HTTP API notation, we will employ C-style pseudocode, or, to be more precise, JavaScript-like or Python-like since types are omitted. We assume such imperative structures being readable enough to skip detailed grammar explanations.
The approach we use to design APIs comprises four steps:
This four-step algorithm actually builds an API from top to bottom, from common requirements and use case scenarios down to a refined entity nomenclature. In fact, moving this way will eventually conclude with a ready-to-use API — that's why we value this approach highly.
It might seem that the most useful pieces of advice are given in the last chapter, but that's not true. The cost of a mistake made at certain levels differs. Fixing the naming is simple; revising the wrong understanding of what the API stands for is practically impossible.
-NB. Here and throughout we will illustrate API design concepts using a hypothetical example of an API allowing for ordering a cup of coffee in city cafes. Just in case: this example is totally synthetic. If we were to design such an API in a real world, it would probably have very few in common with our fictional example.
NB. Here and throughout we will illustrate API design concepts using a hypothetical example of an API allowing for ordering a cup of coffee in city cafes. Just in case: this example is totally synthetic. If we were to design such an API in a real world, it would probably have very few in common with our fictional example.
Key question you should ask yourself looks like that: what problem we solve? It should be asked four times, each time putting an emphasis on an another word.
So, let's imagine that we are going to develop an API for automated coffee ordering in city cafes, and let's apply the key question to it.
* Possibly, we're solving knowledge and selection problems? To provide humans with a full knowledge what options they have right now and right here.
-* Possibly, we're optimizing waiting times? To save the time people waste waiting their beverages.
-* Possibly, we're reducing the number of errors? To help people get exactly what they wanted to order, stop losing information in imprecise conversational communication or in dealing with unfamiliar coffee machine interfaces?
-
-‘Why’ question is the most important of all questions you must ask yourself. And not only about global project goals, but also locally about every single piece of functionality. If you can't briefly and clearly answer the question ‘what for this entity is needed’, then it's not needed.
-Here and throughout we assume, to make our example more complex and bizarre, that we are optimizing all three factors.
-Why would someone need an API to make a coffee? Why ordering a coffee via ‘human-to-human’ or ‘human-to-machine’ interface is inconvenient, why have ‘machine-to-machine’ interface?
+‘Why’ question is the most important of all questions you must ask yourself. And not only about global project goals, but also locally about every single piece of functionality. If you can't briefly and clearly answer the question ‘what for this entity is needed’, then it's not needed.
+Here and throughout we assume, to make our example more complex and bizarre, that we are optimizing all three factors.
+Do the problems we outlined really exist? Do we really observe unequal coffee-machines utilization in mornings? Do people really suffer from inability to find nearby a toffee nut latte they long for? Do they really care about minutes they spend in lines?
‘Separate abstraction levels in your code’ is possibly the most general advice to software developers. However, we don't think it would be a grave exaggeration to say that abstraction levels separation is also the most difficult task to API developers.
Before proceeding to the theory, we should formulate clearly why abstraction levels are so important, and what goals we're trying to achieve by separating them.
Let us remember that software product is a medium connecting two outstanding contexts, thus transforming terms and operations belonging to one subject area into another area's concepts. The more these areas differ, the more interim connecting links we have to introduce.
@@ -791,7 +802,7 @@ It is important to note that we don't calculate new variables out from sensors dWe will discuss data contexts in more details in the Section II. Here we will just state that data flows and their transformations might be and must be examined as a specific API facet, which, from one side, helps us to separate abstraction levels properly, and, from other side, to check if our theoretical structures work as intended.
We will discuss data contexts in more details in the Section II. Here we will just state that data flows and their transformations might be and must be examined as a specific API facet, which, from one side, helps us to separate abstraction levels properly, and, from other side, to check if our theoretical structures work as intended.
Basing on the previous chapter, we understand that the abstraction hierarchy in our hypothetical project would look like that:
Such decomposed API is much easier to read than a long sheet of different attributes. Furthermore, it's probably better to group even more entities in advance. For example, place
and route
could be joined in a single location
structure, or offer
and pricing
might be combined into a some generalized object.
It is important to say that readability is achieved not only by mere grouping the entities. Decomposing must be performed in such a manner that a developer, while reading the interface, instantly understands: ‘here is the place description of no interest to me right now, no need to traverse deeper’. If the data fields needed to complete some action are scattered all over different composites, the readability degrades, not improves.
-Proper decomposition also helps extending and evolving the API. We'll discuss the subject in the Section II.
Proper decomposition also helps extending and evolving the API. We'll discuss the subject in the Section II.
When all entities, their responsibilities, and relations to each other are defined, we proceed to developing the API itself. We are to describe the objects, fields, methods, and functions nomenclature in details. In this chapter we're giving purely practical advice on making APIs usable and understandable.
Important assertion at number 0:
Sometimes explicit location passing is not enough since there are lots of territorial conflicts in a world. How the API should behave when user coordinates lie within disputed regions is a legal matter, regretfully. Author of this books once had to implement a ‘state A territory according to state B official position’ concept.
Important: mark a difference between localization for end users and localization for developers. Take a look at the example in #12 rule: localized_message
is meant for the user; the app should show it if there is no specific handler for this error exists in code. This message must be written in user's language and formatted according to user's location. But details.checks_failed[].message
is meant to be read by developers examining the problem. So it must be written and formatted in a manner which suites developers best. In a software development world it usually means ‘in English’.
Worth mentioning is that localized_
prefix in the example is used to differentiate messages to users from messages to developers. A concept like that must be, of course, explicitly stated in your API docs.
And one more thing: all strings must be UTF-8, no exclusions.
And one more thing: all strings must be UTF-8, no exclusions.
+