diff --git a/docs/API.en.epub b/docs/API.en.epub index c8dbcaa..d596362 100644 Binary files a/docs/API.en.epub and b/docs/API.en.epub differ diff --git a/docs/API.en.html b/docs/API.en.html index 1ee54f7..ea3aa20 100644 --- a/docs/API.en.html +++ b/docs/API.en.html @@ -240,6 +240,16 @@ a.anchor { · https://www.linkedin.com/in/twirl/
++ The API-first development is one of the hottest technical topics in + 2020, since many companies started to realize that API serves as a + multiplicator to their opportunities—but it also amplifies the design + mistakes as well. +
++ The book is dedicated to designing APIs: how to build the architecture + properly, from a high-level planning down to final interfaces. +
Illustrations by Maria Konstantinova
@@ -265,7 +275,7 @@ a.anchor {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.
@@ -304,7 +314,7 @@ a.anchor {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
.The POST /orders
handler checks all order parameters, puts a hold of corresponding sum on user's credit card, forms a request to run, and calls the execution level. First, correct execution program needs to be fetched:
POST /v1/programs/match
+POST /v1/program-matcher
{ "recipe", "coffee-machine" }
→
{ "program_id" }
@@ -669,7 +679,7 @@ GET /sensors
Please note that knowing the coffee machine API kind isn't required at all; that's why we're making abstractions! We could possibly make interfaces more specific, implementing different run
and match
endpoints for different coffee machines:
-POST /v1/programs/{api_type}/match
+POST /v1/program-matcher/{api_type}
POST /v1/programs/{api_type}/{program_id}/run
This approach has some benefits, like a possibility to provide different sets of parameters, specific to the API kind. But we see no need in such fragmentation. run
method handler is capable of extracting all the program metadata and perform one of two actions:
@@ -885,7 +895,7 @@ let recipes = api.getRecipes();
// Retrieve a list of all available coffee machines
let coffeeMachines = api.getCoffeeMachines();
// Build a spatial index
-let coffeeMachineRecipesIndex = buildGeoIndex(recipes, coffee-machines);
+let coffeeMachineRecipesIndex = buildGeoIndex(recipes, coffeeMachines);
// Select coffee machines matching user's needs
let matchingCoffeeMachines = coffeeMachineRecipesIndex.query(
parameters,
@@ -901,7 +911,7 @@ app.display(coffeeMachines);
- display nearby cafes where a user could order a particular type of coffee — for users seeking a certain beverage type.
Then our new interface would look like:
-POST /v1/coffee-machines/search
+POST /v1/offers/search
{
// optional
"recipes": ["lungo", "americano"],
@@ -924,16 +934,16 @@ app.display(coffeeMachines);
- an
offer
— is a marketing bid: on what conditions a user could have the requested coffee beverage (if specified in request), or a some kind of marketing offering — prices for the most popular or interesting products (if no specific preference was set);
- a
place
— is a spot (café, restaurant, street vending machine) where the coffee machine is located; we never introduced this entity before, but it's quite obvious that users need more convenient guidance to find a proper coffee machine than just geographical coordinates.
-NB. We could have been enriched the existing /coffee-machines
endpoint instead of adding a new one. This decision, however, looks less semantically viable: coupling in one interface different modes of listing entities, by relevance and by order, is usually a bad idea, because these two types of rankings implies different usage features and scenarios.
+NB. We could have been enriched the existing /coffee-machines
endpoint instead of adding a new one. This decision, however, looks less semantically viable: coupling in one interface different modes of listing entities, by relevance and by order, is usually a bad idea, because these two types of rankings implies different usage features and scenarios. Furthermore, enriching the search with ‘offers’ pulls this functionality out of coffee-machines
namespace: the fact of getting offers to prepare specific beverage in specific conditions is a key feature to users, with specifying the coffee-machine being just a part of an offer.
Coming back to the code developers are writing, it would now look like that:
-// Searching for coffee machines
+// Searching for offers
// matching a user's intent
-let coffeeMachines = api.search(parameters);
+let offers = api.search(parameters);
// Display them to a user
-app.display(coffeeMachines);
+app.display(offers);
Helpers
-Methods similar to newly invented coffee-machines/search
are called helpers. The purpose they exist is to generalize known API usage scenarios and facilitate implementing them. By ‘facilitating’ we mean not only reducing wordiness (getting rid of ‘boilerplates’), but also helping developers to avoid common problems and mistakes.
+Methods similar to newly invented offers/search
are called helpers. The purpose they exist is to generalize known API usage scenarios and facilitate implementing them. By ‘facilitating’ we mean not only reducing wordiness (getting rid of ‘boilerplates’), but also helping developers to avoid common problems and mistakes.
For instance, let's consider the order price question. Our search function returns some ‘offers’ with prices. But ‘price’ is volatile; coffee could cost less during ‘happy hours’, for example. Developers could make a mistake thrice while implementing this functionality:
- cache search results on a client device for too long (as a result, the price will always be nonactual);
@@ -1055,7 +1065,7 @@ The invalid price error is resolvable: client could obtain a new price offer and
Let's try to group it together:
{
- "results": {
+ "results": [{
// Place data
"place": { "name", "location" },
// Coffee machine properties
@@ -1073,7 +1083,7 @@ The invalid price error is resolvable: client could obtain a new price offer and
"pricing": { "currency_code", "price", "localized_price" },
"estimated_waiting_time"
}
- }
+ }, …]
}
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.
@@ -1895,6 +1905,147 @@ POST /v1/orders
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.
Chapter 12. Annex to Section I. Generic API Example
+Let's summarize the current state of our API study.
+1. Offer search
+POST /v1/offers/search
+{
+ // optional
+ "recipes": ["lungo", "americano"],
+ "position": <geographical coordinates>,
+ "sort_by": [
+ { "field": "distance" }
+ ],
+ "limit": 10
+}
+→
+{
+ "results": [{
+ // Place data
+ "place": { "name", "location" },
+ // Coffee machine properties
+ "coffee-machine": { "brand", "type" },
+ // Route data
+ "route": { "distance", "duration", "location_tip" },
+ "offers": {
+ // Recipe data
+ "recipe": { "id", "name", "description" },
+ // Recipe specific options
+ "options": { "volume" },
+ // Offer metadata
+ "offer": { "id", "valid_until" },
+ // Pricing
+ "pricing": { "currency_code", "price", "localized_price" },
+ "estimated_waiting_time"
+ }
+ }, …],
+ "cursor"
+}
+
+2. Working with recipes
+// Returns a list of recipes
+// Cursor parameter is optional
+GET /v1/recipes?cursor=<cursor>
+→
+{ "recipes", "cursor" }
+
+// Returns the recipe by its id
+GET /v1/recipes/{id}
+→
+{ "recipe_id", "name", "description" }
+
+3. Working with orders
+// Creates an order
+POST /v1/orders
+{
+ "coffee_machine_id",
+ "currency_code",
+ "price",
+ "recipe": "lungo",
+ // Optional
+ "offer_id",
+ // Optional
+ "volume": "800ml"
+}
+→
+{ "order_id" }
+
+// Returns the order by its id
+GET /v1/orders/{id}
+→
+{ "order_id", "status" }
+
+// Cancels the order
+POST /v1/orders/{id}/cancel
+
+4. Working with programs
+// Returns an identifier of the program
+// corresponding to specific recipe
+// on specific coffee-machine
+POST /v1/program-matcher
+{ "recipe", "coffee-machine" }
+→
+{ "program_id" }
+
+// Return program description
+// by its id
+GET /v1/programs/{id}
+→
+{
+ "program_id",
+ "api_type",
+ "commands": [
+ {
+ "sequence_id",
+ "type": "set_cup",
+ "parameters"
+ },
+ …
+ ]
+}
+
+5. Running programs
+// Runs the specified program
+// on the specefied coffee-machine
+// with specific parameters
+POST /v1/programs/{id}/run
+{
+ "order_id",
+ "coffee_machine_id",
+ "parameters": [
+ {
+ "name": "volume",
+ "value": "800ml"
+ }
+ ]
+}
+→
+{ "program_run_id" }
+
+// Stops program running
+POST /v1/runs/{id}/cancel
+
+6. Managing runtimes
+// Creates a new runtime
+POST /v1/runtimes
+{ "coffee_machine_id", "program_id", "parameters" }
+→
+{ "runtime_id", "state" }
+
+// Returns the state
+// of the specified runtime
+GET /v1/runtimes/{runtime_id}/state
+{
+ "status": "ready_waiting",
+ // Command being currently executed
+ // (optional)
+ "command_sequence_id",
+ "resolution": "success",
+ "variables"
+}
+
+// Terminates the runtime
+POST /v1/runtimes/{id}/terminate
+