買彩券也能理解 API!從生活案例帶你一次搞懂 RESTful API

買彩券也能理解 API!從生活案例帶你一次搞懂 RESTful API

前言

接下來,就讓我們從一個簡單的彩券案例開始,一步步揭開 API、REST、以及 RESTful API 的神祕面紗。

APIs

很多時候我們會常常聽到人家說:

「這個有 APIs 可以用嗎?」

「這個有提供 APIs 嗎?」

「你會接 APIs 嗎?」。

身為一名工程師,相信你一定聽過不少次 APIs 這個單字,那麼究竟什麼是 APIs 呢?

APIs 全名是 Application Programming Interface 中文翻譯為應用程式介面,大多文獻、參考來源都會說它是一個應用程式與應用程式之間的橋樑、接口,我認為這樣的定義與說明對於一個新手來講是相對難懂的,所以我想請你把這些介紹與說明拋到腦後。

「拋到腦後的話,那你該怎麼解釋?」

我知道你會這樣講,所以別擔心,我將會用現實生活中的例子來解釋 APIs,讓你更容易理解。

其實 APIs 是一種「交易」行為

請你試著在腦海中模擬出一個畫面…

你今天要去彩券行買彩券,當你走進去後,你會跟彩券行老闆說:「老闆,我要 100 塊大樂透!」,接著你就會把 100 塊給老闆,老闆確定有收到 100 塊後,就會在彩券機上輸入你的 100 塊訂單,接著彩券機就會印出一張大樂透,裡面有兩組號碼,然後老闆就會把這張大樂透給你。

Note
通常彩卷一組號碼(6 個號碼)是 50 元,而這邊舉例買了兩組號碼,所以是 100 元。

這個過程中,我們就已經解釋了整個 APIs 的概念跟行為,什麼意思呢?讓我們將 APIs 與買彩券的行為做一個對應,這樣你就會更容易理解了。

首先我們先了解一下這個對應表:

  • 你:使用者(User)
  • 老闆:伺服器(Server)
  • 100 塊:請求(Request),通常會附帶驗證資料
  • 彩券機:銀行將寫好的 APIs 封裝在裡面,讓伺服器可以直接呼叫對應的功能,當然也會包含一些驗證資料、參數等
  • 大樂透:回傳(Response)的資料(Data)

接著我們將這個對應表套用到我們的例子中:

  • 你今天要去彩券行買彩券,當你走進去:使用者打開瀏覽器,輸入網址進入到特定網站
  • 跟彩券行老闆說:「老闆,我要 100 塊大樂透!」,把 100 塊給老闆:向網站發請一個請求,並附帶驗證資料
  • 老闆確定有收到 100 塊:伺服器驗證你的請求是否合法以及是否有權限
  • 就會在彩券機上輸入你的 100 塊訂單:伺服器去呼叫 APIs,APIs 是一個已經封裝好的函式或程式,通常會有多個 APIs 可以使用
  • 彩券機就會印出一張大樂透:APIs 回傳資料給伺服器
  • 老闆就會把這張大樂透給你:伺服器將資料回傳給使用者

當然這個舉例可能沒有很標準,但至少我認為你已經有一定的概念了。

而這邊 APIs 真正的重點在於「就會在彩券機上輸入你的 100 塊訂單」這件事情上,我們都知道販售彩卷的單位是銀行,但我們不可能買一張彩卷都跑去銀行買,所以我們會去彩券行買,而彩卷機就是幫助我們跟銀行溝通的橋樑,這就是 APIs 的概念。

所以這邊簡單結論一下…

「APIs 其實是 A 應用程式(老闆)與 B 應用程式(銀行)之間的橋樑(彩券機),讓 A 應用程式可以透過 APIs 來跟 B 應用程式溝通,這樣就可以達到資料交換的目的。」

那…如果套用到 Web 前後端開發呢?

  • 使用者可見的網頁介面(前端):A 應用程式
  • 伺服器:作為執行與處理的中心,負責處理後端程式的邏輯
  • APIs:由後端程式提供的接口,讓前端程式可以透過 APIs 來跟後端程式溝通

所以使用者在操作畫面時,其實會跟伺服器說:「嘿!我現在正在請求 /api/v1/user 的資料,你可以幫我把資料回傳給我嗎?」,而伺服器就會去呼叫 /api/v1/user 這個 APIs,然後將資料回傳給使用者,而這個 /api/v1/user 則是會去呼叫一個封裝好的函式或程式,這就是 APIs 的概念。

APIs 的種類

APIs 的種類基本上不外乎是以下這幾種最常見:

  • 開放式 APIs(Open APIs):提供給開發者使用,讓開發者可以串接這些 APIs 來開發自己的應用程式
  • 內部 APIs(Internal APIs):公司內部使用的 APIs,不對外開放
  • 第三方 APIs(Third-party APIs):由第三方提供的 APIs,讓開發者可以串接這些 APIs 來開發自己的應用程式

Note
第三方意旨非官方提供的 APIs,通常是由第三方公司或者個人提供的 APIs。

以 OpenAI 來講,大多人都是使用 ChatGPT 的 WebUI 來跟 AI 對話,但是如果你想要串接 ChatGPT 的 APIs 來開發自己的應用程式,那你就可以使用 OpenAI 提供的 APIs 來串接,而這個 APIs 就隸屬於開放式 APIs。

那麼開放式 APIs 與第三方 APIs 有什麼差別呢?其實差別不大,只是開放式 APIs 比較著重於開放性,舉例來講開放式 APIs 的資料來源可能是由政府開放資料平台提供,但是你如果直接去串接 APIs,可能會有一些資料邏輯上的問題要處理,這時候你就可以使用第三方 APIs 來解決這個問題,因為第三方 APIs 已經幫你處理好了這些問題。

所以這邊簡單結論一下:

  • 開放式 APIs:強調的是公開性與生態系建立,通常由原始服務提供。
  • 第三方 APIs:指由非官方或其他公司基於原始 APIs 再做封裝或增強處理,目的是讓資料整合更便利。
  • 內部 APIs:Just for company.,僅著重於公司內部使用,不對外開放

這邊也額外補充說明一下,如果可以直接使用開放式 APIs,那是最好的,因為第三方 APIs 是由第三方提供,其穩定度以及資料安全性都是一個問題,所以如果可以直接使用開放式 APIs,那是最好的。

RESTful

首先,這邊我要直接先講結論…

「RESTful APIs 是一種設計 APIs 的風格,而不是一種技術或標準,僅僅只是一個約定俗成的方式。」

前面我們已經知道什麼是 APIs,但 RESTful…又是什麼鬼東西呢?如果你去 Google 應該會得到一個「表現層狀態轉換」的翻譯文字,但這樣的翻譯對於一個新手來講是非常難懂的,就算是我也是。

所以在解釋 RESTful 之前,我們要先知道什麼是 REST。

REST 是什麼?

REST 是 REpresentational State Transfer 的縮寫,中文翻譯確實是「表現層狀態轉換」,目的是為了方便管理分散的資源所提出來的一套設計模式,而這套設計模式是由 Roy Fielding 在 2000 年博士論文中提出來的。

只要你設計出來的 APIs 符合 REST 的設計風格,那這個 APIs 就可以稱為 RESTful APIs。

那麼想要符合 REST 的設計風格,你需要遵守以下這幾個原則:

Uniform Interface(統一介面)

要達成統一介面這件事情,其實是有一些事情要注意,通常必須要達成四項約束:

  • 資源識別:每一個資料請求都要有一個獨特的 Url 來識別資源(如:/api/v1/user),就跟前端在取得特定元素時會使用 document.getElementById('id') 一樣。
  • 資源操作:伺服器再回傳資料時,一率都是統一一種格式,例如:JSON 格式,這樣串接的開發者就不用再重新學習新格式的問題,現今開發也都大多採用 Content-Type: application/json 這種格式
  • 自我描述:伺服器回傳的資料中,必須要有足夠的資訊讓開發者可以理解,不需要再去查閱文件,例如:HTTP 狀態碼、錯誤訊息等,例如:HTTP Code: 200 代表成功,HTTP Code: 400 代表錯誤,接著再回傳一些錯誤訊息,像是 {"message": "錯誤訊息"} 這樣
  • 超媒體應用狀態:簡單來講就是,當你在取得資料時,伺服器會回傳一些超連結,好處在於前端不用再去處理 Url,而是透過伺服器回傳的超連結來取得資料,只需要後端去維護這些超連結即可,這樣可以讓前端更專注在畫面的呈現上,通常裡面會包含 self(當前) 、next(下一筆) 、prev(上一筆) 、first(第一筆) 、last(最後一筆) 等等連結。

基本上只要符合上述這四項約束,就可以稱為符合 Uniform Interface 這個原則。

Note
以我目前自己開發經驗來講,超媒體應用狀態這件事情比較常見在管理側邊欄的欄位連結,例如…當你登入後,後台並不會直接呈現所有前端的頁面連結,而是透過後端驗證之後,回傳給你前端的頁面連結。

Stateless(無狀態)

RESTful APIs 必須是無狀態的,這邊所謂的無狀態是指,每一次的請求都是獨立的,不會受到前一次請求的影響,這樣的好處在於伺服器不需要去維護使用者的狀態,只需要處理請求即可。

不好懂的話,這邊舉例使用者登入的情境,當使用者登入後,伺服器會回傳一個 Token 給使用者

1
2
3
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
}

此時這個 Token 將會記憶在使用者的瀏覽器中,當使用者再次發送請求時,只需要帶上這個 Token,伺服器就可以知道這個使用者是誰,而不需要再去維護使用者的狀態(意旨早期 Session 的概念)。

Client-Server(客戶端-伺服器)

Client-Server 是在講分離客戶端與伺服器,使得用客戶端與伺服器之間可以獨立演進,而不會互相影響,在早期的網頁開發中,我們會將 HTML、CSS、JavaScript、後端程式碼全部混在一起,這樣的開發方式是不符合 Client-Server 這個原則的,如果這樣還不好懂的話,更白話文一點就是指「前後端分離」這件事。

Layered System(分層系統)

Layered System 意思是指將整個應用程式切成多的獨立的層級,每一層只做好自己的事情,不會去干涉到其他層級的事情。

最好懂的例子大多都是舉例 MVC 模式:

  • Model:負責處理資料的邏輯
  • View:負責處理畫面的邏輯
  • Controller:負責處理邏輯的控制器

MVC 的經典設計模式中,就可以知道 View 不會直接跟 Model 溝通,而是透過 Controller 來處理,這樣就符合 Layered System 這個原則。

所以 Layered System 概念就像是一個團隊,每個人各司其職,只和隔壁的同事協作,這樣整個工作流程會更順暢。

在應用程式開發中,我們也會把檢查 Token 與處理使用者邏輯分在不同的 Service/Module,這也是一種分層的概念,如果套用到我們 APIs 設計上也就是指 APIs 會分成多層,例如一個使用者登入的行為,就會被拆成以下幾個層級:

  • POST /api/v1/auth/:負責處理驗證的邏輯,但不會去處理使用者的邏輯
  • POST /api/v1/user/:負責處理使用者的邏輯,但不會去處理 Token 的邏輯
  • POST /api/v1/token/:負責處理 Token 的邏輯,但不會去處理使用者的邏輯

接著底下還會區分成更細的層級,例如:POST /api/v1/auth/signinPOST /api/v1/auth/signupPOST /api/v1/auth/signout,這樣就可以達到 Layered System 這個原則。

Cacheable(可快取)

Cacheable 意思是指伺服器回傳的資料可以被快取,這樣可以提升效能,減少伺服器的負擔。

這邊以串接 APIs 來講,我們可以針對一些不常變動的資料進行快取(設定 Cache-Control),這樣當使用者再次發送請求時,就可以直接從快取中取得資料,而不用再次向伺服器發送請求,這樣可以提升效能。

但這部分比較偏向由後端工程師來處理,例如:

1
2
3
4
5
app.get('/api/v1/user', (req, res) => {
// 設定快取時間為 1 小時
res.setHeader('Cache-Control', 'max-age=3600')
res.json({ message: 'Hello World' })
})

接下來當使用者再次發送請求時,就可以直接從快取中取得資料,而不用再次向伺服器發送請求,這樣可以提升效能。

Code on Demand(可程式碼化)

這個並不是非必要的,但以 Code on Demand 這個原則來講,就是指伺服器可以回傳一些程式碼給客戶端執行,這樣可以提升客戶端的功能性,只是這個 Code on Demand 通常不會是必要的,就算是我也還沒遇過這種需求。

RESTful + APIs = RESTful APIs

前面我們認識了什麼是 APIs,也了解了什麼是 REST,那麼當這兩個東西組合在一起時,只要符合 REST 的設計風格,那這個 APIs 就可以稱為 RESTful APIs,所以這一篇我將會以 Web 開發領域的角度去說明該如何設計 RESTful APIs。

首先我們先假設一個情景,也就是今天有一個電商系統,目前線稿圖上有以下功能:

  • 使用者登入
  • 使用者註冊
  • 使用者登出
  • 取得商品列表
  • 取得商品詳細資訊
  • 加入購物車
  • 刪除購物車商品
  • 結帳

你可以試著暫停一下,你會如何設計這些 APIs 路徑(溝通橋樑)呢?這邊就舉例我真人真事遇過的例子,當時我在 A 公司,那間公司的後端工程師就是這樣設計的:

  • 使用者登入:POST /login
  • 使用者註冊:POST /register
  • 使用者登出:POST /logout
  • 取得商品列表:GET /products
  • 取得商品詳細資訊:GET /products/:id
  • 加入購物車:POST /cart
  • 刪除購物車商品:POST /cart/:id
  • 結帳:POST /checkout
  • 建立商品(後台):POST /admin/products
  • 刪除商品(後台):POST /admin/products/:id

感覺好像沒有什麼,接著我離職後到另一間公司,那間公司的後端工程師就是這樣設計的:

  • 使用者登入:POST /userSignIn
  • 使用者註冊:POST /userSignUp
  • 使用者登出:POST /userSignOut
  • 取得商品列表:GET /productList
  • 取得商品詳細資訊:GET /productDetail
  • 加入購物車:POST /addCart
  • 刪除購物車商品:POST /deleteCart
  • 結帳:POST /checkout
  • 建立商品(後台):POST /adminCreateProduct
  • 刪除商品(後台):POST /adminDeleteProduct

雖然看起來好像還算 ok,沒有什麼問題,但是這個問題就會變成每一間公司都有屬於自己的特色命名方式,那麼這樣的命名就會有以下問題:

  1. 不容易使用現代化工具(標準化工具)來產生文件,如:Swagger、Postman 這些都是基於 RESTful APIs 來設計的
  2. 以「動作」為主的命名方式,容易導致發生所謂的 RPC(Remote Procedure Call)風格,導致資源不明確,路徑也會因此複雜化
  3. 不符合 HTTP Method 的規範,例如:POST(新增)、GET(取得)、PUT(更新)、DELETE(刪除),開發者難以透過 HTTP Method 來判斷這個 APIs 是做什麼的
  4. 難以直觀的瞭解到這個 APIs 是做什麼的,容易有各自解釋的問題,動詞與名詞上會有不同的命名方式與解釋
  5. 對於版本與路徑的管理不易,後續若要新增功能或者修改功能,會導致路徑的變動,這樣會對前端開發者造成困擾
  6. 每進一間公司就要重新學習他們的 APIs or 閱讀文件才能知道這個 APIs 是做什麼的

Note
RPC 是另一種設計方式,通常是以「動作」為主的命名方式,也需要知道相關參數才能夠使用。

因此這邊如果將這些 APIs 路徑重新設計一下,那應該要怎麼設計呢?這邊我將會以 RESTful APIs 的設計風格來重新設計這些 APIs 路徑:

  • 使用者登入:POST /api/v1/auth/signin
  • 使用者註冊:POST /api/v1/auth/signup
  • 使用者登出:POST /api/v1/auth/signout
  • 取得商品列表:GET /api/v1/products
  • 取得商品詳細資訊:GET /api/v1/products/:id
  • 加入購物車:POST /api/v1/cart
  • 刪除購物車商品:DELETE /api/v1/cart/:id
  • 結帳:POST /api/v1/checkout
  • 建立商品(後台):POST /api/v1/admin/products
  • 刪除商品(後台):DELETE /api/v1/admin/products/:id

接著這些格式請求(Request)與回應(Response)的格式,一率都是統一使用 JSON 格式(Content-Type: application/json) 進行傳輸,你可能會問:

「為什麼要使用 JSON 格式呢?」

最主要原因是,JSON 格式是一種輕量級的資料交換格式,由於 JSON 是輕量且易讀的資料交換格式,多數程式語言都有支援,因此前後端開發者能以最低成本解析與調整。

由於我們是採用 Web 開發來講,所以就會搭配 HTTP Method 來設計這些 APIs 路徑:

  • POST:新增資料(or 資料狀態的改變)
  • GET:取得資料
  • PUT:更新資料
  • PATCH:部分更新資料
  • DELETE:刪除資料

接著會搭配 HTTP Code 來回傳資料,例如:

  • HTTP Code: 200:成功
  • HTTP Code: 400:錯誤
  • HTTP Code: 401:未授權
  • HTTP Code: 404:找不到資源
  • HTTP Code: 500:伺服器錯誤

因此,一個符合 RESTful APIs 風格的 APIs 會像以下這樣:

使用者登入:POST /api/v1/auth/signin

Request(請求):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const data = {
email: '[email protected]',
password: '123456'
}

const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}

fetch('/api/v1/auth/signin', options)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))

Response(回應, HTTP Code: 200):

1
2
3
4
{
"message": "登入成功",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
}

Note
上方 Token 稱之為 JWT Token,如果你對於 JWT Token 不熟悉,可以參考這篇文章:關於 JWT 驗證

Response(回應, HTTP Code: 400):

1
2
3
4
{
"message": "登入失敗",
"error": "帳號或密碼錯誤"
}

這邊也提供一下 RESTful APIs 與原本的三者比較表格給你參考:

RESTful APIs A 公司 B 公司
使用者登入 POST /api/v1/auth/signin POST /login POST /userSignIn
使用者註冊 POST /api/v1/auth/signup POST /register POST /userSignUp
使用者登出 POST /api/v1/auth/signout POST /logout POST /userSignOut
取得商品列表 GET /api/v1/products GET /products GET /productList
取得商品詳細資訊 GET /api/v1/products/:id GET /products/:id GET /productDetail
加入購物車 POST /api/v1/cart POST /cart POST /addCart
刪除購物車商品 DELETE /api/v1/cart/:id POST /cart/:id POST /deleteCart
結帳 POST /api/v1/checkout POST /checkout POST /checkout
建立商品(後台) POST /api/v1/admin/products POST /admin/products POST /adminCreateProduct
刪除商品(後台) DELETE /api/v1/admin/products/:id POST /admin/products/:id POST /adminDeleteProduct

透過上述講解與範例,相信你對於為什麼我們會推薦你開 APIs 時,使用 RESTful APIs 的設計風格,應該有一定的概念囉~

結語

但這邊最後要特別提醒一件事情,我們這邊所使用的 HTTP Method 並不等於 REST,也不等於 REST 是絕對的規範,這僅僅只是一種約定俗成的方式,只要符合 REST 的設計風格,那這個 APIs 就可以稱為 RESTful APIs。

RESTful 雖然主流,但也不代表唯一解;如果需要複雜查詢或縮減資料傳輸,GraphQL 可能更適合,在某些企業內部會用 SOAP / RPC 等傳統方案哩。

另外,通常來講不會在意 API 的路徑長度,因為 RESTful API 的設計核心理念在於資源的表現,而不是在意路徑的長度,所以不要因為路徑長度而去犧牲資源的表現,導致資源不明確、開發者難以理解哩。

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ