這章在講設計 API 的 best practice。

Designing for Real-Life Use Cases

設計 API 時要以特定的、實際的 use case 進行決策,不要空想。

千萬不要公開內部基礎架構,把焦點放在與外部開發者(或 API 使用者)與 API 的互動體驗上。

藉由選擇特定的工作流程或 use case,你可以把重點放在一項設計上,並測試它是否可以幫助你的使用者。

在 brainstorming 階段去想「如果怎樣的話…」是有幫助的,可是到了設計階段,太多的「如果怎樣的話」的想像,反而會讓設計失焦,所以針對特定的 use case 是比較好的。

Designing for a Great Developer Experience

提供開發者好的開發體驗。

讓 API 能更快、更容易上手

文件可以幫開發者上手,有 tutorial 跟 getting started guide 也很好。

也可以提供線上互動式文件、線上的 sandbox 來讓 developer 可以實際測試。

提供 SDK 也可以幫助 developer 使用 API。

最後是應該要讓 developer 不需要登入或註冊就能使用 API。如果一定得要註冊,要盡可能減少註冊需要的資料。如果 API 使用 OAuth 保護,那麼應該要能讓 developer 在 UI 中產生 token 以使用 API。

維持一致性

像是 entry nmae、request 參數、response 等等,應該要維持一致性,讓 developer 即使不看文件也能猜到部份的 API。

在漸進修改的過程中,盡量與既有的設計模式保持一致,對使用者來說是最好的做法。

不要讓同樣的東西使用不同的名稱。

一致性很重要的原因是它可以減少試著了解你的 API 的 developer 的認知負擔(cognitive load)

Make Troubleshooting Easy

藉由回傳有意義的 error 跟 building tool 做到這點。

設計 API 時應該有系統的組織跟分類錯誤以及他們的回傳方式來方便 developer 排除問題。

有意義的 error 容易了解、明確而且可以讓人採取行動。它們可以協助 developer 了解問題並處理它。提供這些 error 的 detail 可以帶來較佳的使用者體驗。

machine-readable 的 error code 字串可以讓 developer 以程式處理錯誤。

除了 machine-readable 的 error code 之外,也可以加入 human-readable 的敘述,來讓 developer 更了解發生了什麼問題。

error 要講出具體錯誤的原因,像是「token 因為被撤銷而造成驗證失敗」,用 token_revoked 比 invalid_auth 好。

幫 error 分類

將 API request 過程(從 request 開始,到 architecture 的各種 service 邊界)的各種 high-level error 分門別類,例如:

按照程式碼路徑將 error 分類後,要考慮對這些 error 而言,採取哪個 level 的 communication 是有意義的。一種方式是在 response payload 中放入 HTTP status code 跟 header,以及 machine-readable 的 code 或更詳細的 human-readable 的錯誤訊息。

大部分情況下,要盡量具體的說明來讓 developer 採取正確的後續動作。但有些時候,尤其跟安全有關的時候,可能要回傳比較籠統的資訊,不然原始訊息可能會透漏如資料庫 connection 資訊等會引發安全問題的資訊。

在程式結構上,可以用一套相同的 library 檢查 request 並將 error format 成相同的格式來 response。

將 error 文件化,像是寫在 API 文件中。

與 HTTP API 錯誤與問題相關可以參考 RFC 7807

Build tooling

log HTTP status、error、error 的頻率以及其他 request metadata,好方便在內部或外部進行 debug 或處理問題。

建立 dashboard 來協助 developer 分析 API request 的 metadata,例如可以統計最常用的 API entry、找出沒被用過的 API 參數、分類常見錯誤等等。

log 跟 dashboard 都有很多現成的工具。

Make Your API Extensible

要擬定 API 的發展策略,讓 API 是可擴展的(extensible)。

API 應該提供可開啟新工作流程的基本元素,而非只是對映你的 app 的工作流程。API 的建立方式決定了 API 的使用者可用他來做什麼事情。如果你提供太低階的操作,可能會讓整合者負擔太多工作。如果你提供太高階的操作,可能會讓大多數的整合只是對應你自己的 app 所作的工作而已。為了實踐創新,你必須找到適當的平衡點,讓使用者能夠啟動不屬於你的 app 或 API 本身的工作流程。

在前後端分離的架構下,要區分內部使用跟對外公開的 API。兩者對於要提供什麼樣的 API 會有不同的考量。

extensible 的其中一個部份是確保 top partner 有提供 feedback 的機會。要設法 release 某些功能給 top partner 用用看,讓他們給予 feedback。

在想要用版本管理 API 時,如果早期就加入版本管理系統會比較容易,越晚加入越難實作。版本管理系統的好處在於它可以讓你用新版本進行 breaking changes,同時又能維持就版本的回溯相容性(backward compatibility)。breaking changes 就是會讓之前使用你的 API 可以正常運作的 app 無法繼續運作的 changes。

不過,維護版本是需要成本的。如果很多年沒有能力支援舊版本,或者認為 API 不太需要變動,那麼可以不用版本,改用 additive(附加式)變動策略,在單一、穩定的版本中維持 backward compatibility。

如果預計在「未來任何時刻」都有可能出現 breaking changes 與更新,最好建立版本管理系統。在一開始就建立版本管理系統需要付出的成本比之後迫切需要它的時候才加入低得多。

Auth

把 authentication 資訊寫在 ~/.pgpass,格式如下:

1
host:port:database:user:password

在用 pg_dumppsql 時就可以不用輸入密碼。

當然這個檔案的權限要是 600

Backup

1
2
3
4
5
6
7
8
$ pg_dump \
--dbname="[DBNAME]" \
--file="[FILEPATH]" \
--inserts --create \
-h [HOST] \
-p [PORT] \
-U [USER] \
-w

Restore

1
psql -d [DBNAME] -f [SQL_FILE_PATH] -h [HOST] -p [PORT] -U [USER]

ECR 全名是 Elastic Container Registry,是 Amazon 的 docker container registry。

安裝 AWS CLI 後先 aws configure 設定

Push

  1. 做個 docker image
  2. authenticate
    $ aws ecr get-login-password --region [region] | sudo docker login --username AWS --password-stdin [AWSUserID].dkr.ecr.[region].amazonaws.com
  3. create repository,例如 hello-ecr
  4. 幫 image 上 tag,例如 [AWSUserID].dkr.ecr.us-east-2.amazonaws.com/hello-ecr:latest
  5. docker push

Run

一樣先 login,接著 docker run,例如:$ docker run [AWSUserID].dkr.ecr.us-east-2.amazonaws.com/hello-ecr:latest

Ref

在 Go 裡,method 的 receiver 是用 *Obj 還是用 Obj 會有不同的行為。

來個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v Vertex) moveX(movement float64) {
v.X += movement
}

func (v *Vertex) moveY(movement float64) {
v.Y += movement
}

func main() {
v := Vertex{X: 10, Y: 20}

log.Println(v.X, v.Y, v.Abs()) // 10 20 22.360679774997898

v.moveX(2)
log.Println(v.X, v.Y, v.Abs()) // 10 20 22.360679774997898

v.moveY(3)
log.Println(v.X, v.Y, v.Abs()) // 10 23 25.079872407968907
}

v 可以看成像參數。

Vertex 就是 copy by value,caller 跟 callee 的 Vertex instance 是不同的。

*Vertex 就像 C 語言 pointer 參數,本質上還是 copy by value 但因為是 pointer,所以在 moveY() 中的 v 變成是指向 caller 的那個 Vertex instance。

基本上 method 會動到 struct 內的 field 內容都會用 pointer。習慣上當有一個 method 的 receiver 是用 pointer 時,所有 method 的 receiver 都會用 pointer。

環境

  • Synology NAS 型號:DS920+
  • DSM 版本:DSM 6.2.4-25556

建立 Private Docker Registry 步驟

  1. download registry image from Docker hub
  2. start registry container
  3. ssh 進 NAS
  4. /var/packages/Docker/etc/ 編輯 dockerd.json
  5. 加入 "insecure-registries":["host:port"]
  6. 重新啟動 docker (我是把套件停用再啟用啦…)

An API paradigmdefines the interface exposing backend data of a service to otherapplications.

Request-Response API

Request-Response API 通常透過 HTTP web server 來公開 interface。

這種 API 會定義一些 endpoints,client 對這些 endpoints 發出 HTTP request 來索取資料,server 則給予 response。response 通常是 JSON 或 XML 格式。

Request-Response API 有三種:

  • REST
  • RPC
  • GraphQL

REST (Representational State Transfer)

REST is all about resource.

resource 是可以在 web 上被 identify、named、addressed 或 handled 的 entity。

REST API 將資料當成 resource 來 expose 出去,並使用 standard HTTP method 表示 CRUD 的動作。

REST API 遵循的一般規則:

  • resource 是 URL 的一部分,例如 /users
  • 每個 resource 通常有兩個 URL。一個表示 collection,例如 /users。一個表示特定元素,例如 /users/U123
  • resource 使用名詞而非動詞,例如用 /users/U123,而不是 /getUserInfo/U123
  • GETPOSTUPDATEDELETE 等 HTTP method 來告訴 server 要執行的動作。
    • Create
      • POST 建立新 resource
    • Read
      • GET 讀取 resource
      • GET request 永遠不會改變 resource 的狀態,不會有 side effect
      • GET method 有 read-only 的意思
      • GET 是 idempotent
    • Update
      • PUT 來 replace resource。
      • PATCH 來對現有 resource 做部份 update。RFC 5789
    • Delete
      • DELETE 來刪除現有 resource。
  • server 回傳標準的 HTTP response status code 來表示成功或失敗
    • 2XX 代表成功
    • 3XX 代表 resource 已被移除
    • 4XX 代表 client 端錯誤
    • 5XX 代表 server 端錯誤
  • REST API 可回傳 JSON 或 XML 格式

Showing relationships

盡量用 subresource 表示只屬於其他 resource 的 resource,不要用 top-level resource 表示它,這樣可以讓使用 API 的 developer 知道它們之間的關係。

例如 Github 的 API:POST /repos/:owner/:repo/issues 是在某個人的某個 repository 底下建立一個 issue。

非 CRUD 操作

有時候 REST API 需要表示非 CRUD 的操作,常見作法如下:

  • 以 resource 的部份欄位來表示動作(action)
    • 例如 Github 要把 repository archive 起來是用 entry PATCH /repos/:owner/:repo 然後 data body 是 {"archived": true}。因為 PATCH entry 的 request data body 是 resource 要被更新的欄位,所以才說是以「resource 的部份欄位」來表示動作。
  • 將操作視為 subresource
    • 例如 Github 的 lock issue 是 PUT /repos/:owner/:repo/issues/:number/lock
  • 有些操作難以用 REST 模式,例如搜尋,這時候通常會在 API URL 直接使用操作的動詞。
    • 例如在 Github 中尋找符合 query 的檔案:GET /search/code?q=:query:

Remote Procedure Call (RPC)

REST 跟 resource 有關,RPC 則跟動作(action)有關。

RPC 的 client 會在 server 上執行一段 code。client 通常會傳 method name 跟 argument 給 server,然後得到 JSON 或 XML。

RPC API 通常遵循兩個規則:

  • endpoint 含有準備執行的 action 的名稱
  • API call 是用最適合的 HTTP verb 來執行:GET 是 read-only request,POST 則是其他。

當 API 公開的動作比 CRUD 封裝的還要細膩且複雜,或是存在與眼前的「資源」無關的 side effect 時,很適合使用 RPC。RPC style 的 API 也可以配合複雜的 resource model,或針對多種類型的 resource 執行的動作。

RPC style 的 API 除了用 HTTP 外也可以用其他 protocol,包括 Apache ThriftgRPC

GraphQL

https://graphql.org

GraphQL 可以讓 client 端定義需要的 data structure,讓 server 以那個 structure 回傳資料。例如以下是送給 Github API 的 GraphQL query 及其 response:

1
2
3
4
5
6
7
8
{
user(login: "saurabhsahni") {
id
name
company
createdAt
}
}

response:

1
2
3
4
5
6
7
8
9
10
{
"data": {
"user": {
"id": "MDQ6VXNlcjY1MDIS",
"name": "Saurabh Sahni",
"company": "Slack",
"createdAt": "2009-03-19T21:00:06Z"
}
}
}

GraphQL 只需要一個 URL endpoint,而且不需要用不同的 HTTP verb 描述操作,只要在 JSON 內容中寫要做的動作就可以了。

GraphQL 的優點

跟 REST 及 RPC 比起來,GraphQL 的優點:

  • 節省多次 round trip
    • client 可以用 nested query 以一個 request 從多個 resource 取得資料
    • 以 REST 來說,要取得多個 resource 資料可能需要很多個 request
  • 不需要 versioning
    • 在 GraphQL API 增加新的欄位跟 type 不會影響既有的 query
    • 要 deprecate 一個欄位也很容易:可以用 log 分析 client 用了哪些欄位,在工具中隱藏某些欄位,並且在沒人用的時候移除它們。
  • 較小的 payload
    • 因為 client 可以明確指定要什麼資料,所以 payload 可以比較小。
    • REST 跟 RPC 常常回傳 client 永遠用不到的資料。
  • Strongly typed
    • GraphQL 是 strongly typed,它的 type checking 會確保 query 的語法是正確且有效的。
  • Introspection
    • GraphiQL 這個瀏覽器 IDE 可以寫 GraphQL query 來試驗跟了解 GraphQL API (就是可以直接玩 API 啦)

GraphQL 的缺點

對提供 GraphQL API 的提供者來說,GraphQL 增加了複雜性,server 需要做額外的工作來解析複雜的 query 跟驗證參數。最佳化 GraphQL query 的效能也很麻煩。

REST vs RPC vs GraphQL

Event-Driven API

如果 service 的資料常常會改變,用 request-response API 的作法 response 很快會過時,這時候使用 API 的 developer 通常會以 polling 來確保得到最新的資料。但如果 polling 頻率太低,可能會在需要即時更新的狀況下無法即時更新 。而 polling 頻率太高則會浪費資源,因為大部分 request 都不會有新資料。

要即時分享 event 資料,有三種方式:WebHook、WebSocket 跟 HTTP Streaming。

WebHook

WebHook 是個接收 HTTP POST(或 GET、PUT 或 DELETE)的 URL。 實作 WebHook 的 API provider 會在某些事情發生時 POST 一個訊息給使用者設置好的 URL,例如信用卡授權的 postback。

提供 WebHook 會引入的複雜性:

  • Failures and retries:為了確保資訊成功 deliver,須建立發生錯誤時的 retry 機制。
  • Security:使用 WebHook 時,API 使用者要驗證從 WebHook 收到的資料,以確保資料是合法的。
  • Firewall:在防火牆後的 app 很難用 WebHook 收資料,得在防火牆上打洞。
  • Noise 雜訊:通常一個 WebHook call 都代表一個 event。如果有成千上萬個 event 在短時間內發生而且必須透過單一 WebHook 來傳送,可能會產生雜訊。

WebSocket

WebSocket is a protocol used to establish a two-way streaming communication channel over a single Transport Control Protocol (TCP) connection.

WebSocket 這個 protocol 通常用在 web client 跟 server 間,有時也會被用來做 server 對 server 的通訊。WebSocket 可以在比較低的 overhead 的情況下開啟 full-duplex 通訊(server 跟 client 可以同時跟對方通訊)。

WebSocket 是運作在 port 80 或 443 上,所以不用在防火牆上另外開 port 來進行連線與通訊。而且使用 WebSocket 也不像 WebHook 得對 internet 打開一個 HTTP endpoint 來接收 event,相對來說比較安全。

WebSocket 適合快速、live 的 streaming 以及長時間(long-lived)的 connection。但不見得適合用在行動裝置或者網路不穩定的地方,因為 client 必須有能力維持 connection,connection 斷了 client 就得重新啟動它。

HTTP Streaming

在 request-response 形式的 HTTP API 裡,client 送出 request 後,會收到一包有限長度的 response。而使用 HTTP Streaming,server 可以透過 client 開啟的 long-lived connection 來持續推送新資料。

To transmit data over a persistent connection from server to client, there are two options. The first option is for the server to set the Transfer-Encoding header to chunked. This indicates to clients that data will be arriving in chunks of newline-delimited strings. For typ‐ical application developers, this is easy to parse.
Another option is to stream data via server-sent events (SSE). This option is great for clients consuming these events in a browser because they can use the standardized EventSource API.

HTTP Streaming is easy to consume. However, one of the issues with it is related to buffering. Clients and proxies often have bufferlimits. They might not start rendering data to the application until a threshold is met. Also, if clients want to frequently change what kind of events they listen to, HTTP Streaming might not be ideal because it requires reconnections.

Event-Driven API 的比較

總結

沒有一體適用的 API paradigm。每種 API paradigm 只適合特定類型的 use case,所以在實際狀況下有可能需要支援多種 paradigm。