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。

老是失憶……

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
# 跑起一個 ubuntu container 並且用 foreground 模式進入 bash
$ sudo docker run -it ubuntu bash

$ sudo docker run -d -p [HOST_PORT]:[CONTAINER_PORT] --name [CONTAINER_NAME] [IMAGE_NAME]:[version]

# 停止並移除 container
$ sudo docker rm --force [CONTAINER]

# 列出 local 有的 image
$ sudo docker images

# 在 Docker Hub 搜尋 image
$ sudo docker search [KEYWORD]

# 在有 Dockerfile 的資料夾 build Docker image
$ sudo docker build --tag [NAME]:[TAG] .

# Remove Docker image
$ sudo docker rmi [IMAGE]

# Login to a Docker registry
# 如果是 private registry 就要 login 才能 push
$ sudo docker login

# Push Docker image
$ sudo docker push [IMAGE_NAME]:[TAG]

用 Go 寫了個可以讀 Readmoo 的閱讀記錄跟劃線的 package。

可以從閱讀記錄拿到基本的書籍資料跟劃線。

細節見 Github

應該要把拿 token 弄得簡單點,現在很手動…

我的 keymap 是 Sublime + Jetbrains 部份按鍵 + 自己設再配 vim 的大雜燴

  • ctrl + shift + p:執行動作(action)
  • ctrl + p:找檔案
  • ctrl + r:檔案中找 symbol
  • ctrl + alt + shift + t:refactor 選單
  • alt + enter:各種神奇功能(?)
  • alt + insert:加入各種 code
  • shift + f6:rename
  • alt + 1:project browse window
  • alt + 3:find window
  • alt + 4:run window
  • alt + 9:git window

TBC…

Go 從 1.13 開始支援 Go Module,可以在 GOPATH 以外的地方建立 go project 並進行套件管理。一直覺得 source code 只能放在 GOPATH 裡超阿雜…

建立 project

GOPATH 以外的地方建立一個 directory,並且在其中執行 go mod init

1
2
3
$ mkdir project
$ cd project
$ go mod init github.com/cjwind/project

會產生 go.mod 檔案,它會記錄 Go module 與使用的 Go 版本:

1
2
3
module github.com/cjwind/project

go 1.15

接下來在這個 directory 裡進行開發跟 build 就都一樣,重點是現在 source code 不用非得放在 GOPATH 裡啦~

套件管理

go get 安裝 package 後,會發現在 go.mod 多了一行 require [package] [version],就表示目前使用的 package 及其 version。

另外可能會出現 require [package] [version] // indirect,這表示是我們使用的 package 所需要的 package。

也可以用 go get [package]@[version] 來指定特定的 package version。

Ref

COPY

COPY 如果 source 是 directory,會 copy directory 的內容,但是 directory 本身不會 copy。

假設有個資料夾叫 css/,底下有兩個 file foo.cssbar.css

1
COPY ./css /workspace/

這樣在 container 裡會變成 /workspace/ 底下有 foo.cssbar.css,而不是 /workspace/css/ 底下有 foo.cssbar.css。想要是 /workspace/css/ 底下有兩個 file 得這樣寫:

1
COPY ./css /workspace/css

Ref:https://docs.docker.com/engine/reference/builder/#copy

使用 embedded struct 做 json 的 marshal 跟 unmarshal 時,json 欄位會省略 struct embedded 欄位的中間名,以比較簡潔的形式呈現。如果 struct 有寫出欄位名稱,json 欄位就會多那一層。

使用 embedded struct

1
2
3
4
5
6
7
8
9
10
11
type Serving struct {
Amount float64
Unit string
}

type Food struct {
Name string
Serving // embedded struct
NutritionInfo
Comment string
}

marshal 結果:

1
2
3
4
5
6
7
8
9
10
{
"Name":"Banana",
"Amount":100,
"Unit":"g",
"Calorie":0,
"Carb":0,
"Fat":0,
"Protein":0,
"Comment":""
}

不使用 embedded struct

1
2
3
4
5
6
type Food struct {
Name string
Serving Serving // not embedded struct
NutritionInfo
Comment string
}

marshal 結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"Name":"Banana",
"Serving":
{
"Amount":100,
"Unit":"g"
},
"Calorie":0,
"Carb":0,
"Fat":0,
"Protein":0,
"Comment":""
}

不用 embeded struct 就會有一層 Serving,用 embedded struct 就會省略 Serving 這層。

submodule 是在 git repos 中使用別的 repos 的方式之一。

git 的 submodule 是記錄一個指到別人 repo 某個 commit 的指標。對主 repo 來說,記錄的只是一個 submodule commit hash。

切到 submodule 的目錄時做 git 操作會是在操作另一個 repo。

加入 submodule

1
$ git submodule add <repo path>

clone 含有 submodule 的 repos

clone 含有 submodule 的 repos 後,submodule 的目錄會是空的,要做以下動作來初始化:

1
2
$ git submodule init
$ git submodule update

git submodule update 會讓 submodule 的內容回到記錄的 commit。

更新 submodule

submodule 的 repo 更新或者想用不同版本(commit)的 submodule 時,要做以下操作:

1
2
3
4
5
$ cd submodule_dir
$ git pull
$ cd ..
$ git a submodule_dir # 更新主 repo 記錄的 submodule commit hash
$ git ci

概念是把 submodule 的 repo 更新或者 checkout 到想要的 commit,再在主 repo 更新記錄的 submodule commit hash。

移除 submodule

1
2
$ git rm -rf submodule_dir
$ vim .git/config # 移掉 submodule 相關設定

.gitmodules

檔案 .gitmodules 會記錄有哪些 submodule。