RESTful API 的設計
RESTful 是一種風格,不是一種標準。
一定要符合 RESTful 嗎?
既然這不是標準,當然沒有符合也沒關係,API (Application Programming Interface)的目的只在於讓不同的程式交換資料,只要能夠達成這個目的,符不符合 RESTful 並不是特別重要。
如何設計
統一的進入點(endpoint)
https://api.github.com
- 對應不同的 http 動詞,來表示不同的功能
GET /repos/:owner/:repo/issues
POST /repos/:owner/:repo/issues
CRUD 對應 GET, POST, PUT, PATCH, DELETE
- GET 取出資源,不可以修改資料
- POST 創建新的資料
- PUT/PATCH 更新資料
- DELETE 刪除資料
透過 HTTP 狀態碼 表示結果
- 200 OK
- 400 Bad Request
- 404 Not Found
- 500 Internal Server Error
客戶端與服務端分開
- 只要進入點沒有改變,兩邊可以獨立開發,不互相影響
- 服務端僅注重提供一致的資源,開發端可以更彈性的使用,所以不同平台可以用相同的 API
命名
- 每個網址代表一種資源(resource)
- /users
- /issues
- 網址只使用名詞,不使用動詞
舉例
創建一個拍賣網站
API 的進入點為 https://api.fakemember.com
對應 http 動詞
- 會員系統,對應 users
- GET https://api.fakemember.com/users/:id 取得會員資料
- POST https://api.fakemember.com/users/ 創建會員
- PUT https://api.fakemember.com/users/:id 更新會員資料
- DELETE https://api.fakemember.com/users/:id 刪除會員
- 商品管理,對應 items
- GET https://api.fakemember.com/items/ 取得商品清單
- GET https://api.fakemember.com/items/:id 取得特定商品資料
- POST https://api.fakemember.com/items/ 新增商品
- PUT https://api.fakemember.com/items/:id 更新特定商品資料
- DELETE https://api.fakemember.com/items/:id 刪除特定商品
在設計上 users 跟 items 通常也會是資料表的名稱,資料表基本上會存放不只一筆的資料,所以會用複數來命名
小結
RESTful API 最主要的目的是便於管理及開發,除了減少服務端與客戶端的相依,也增加使用上的彈性,但是在實務上有時候可能造成反效果。
舉例來說:
- GET https://api.fakemember.com/items/ 取得商品清單
- GET https://api.fakemember.com/items/:id 取得特定商品資料
如果要取得商品的詳細資料,需要先取得商品清單得到 id 以後才可以取得商品的詳細資料,如果資料的結構非常複雜或是設計不良時,可能會造成單一筆資料要 call 很多次 API。
所以現在也漸漸流行起 graph API (graphql) 來替代 RESTful API。
GitHub 目前提供了兩種形式的 API,想體會實際的差異可以嘗試操作
思考
可以在 GitHub GraphQL API Explorer 測試 GraphQL API
這段程式碼可以一次搜尋複雜度很高的資料,但如果是 RESTful API 需要搜尋幾次呢?
- 什麼情境適合 RESTful API ?
- 什麼狀況適合 GraphQL API ?
{
repositoryOwner(login: "twbs") {
repository(name: "bootstrap") {
pullRequests(states: MERGED, first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {
nodes {
author {
login
}
createdAt
updatedAt
url
commits(first: 100) {
nodes {
commit {
additions
deletions
changedFiles
}
}
totalCount
}
comments(first: 100) {
nodes {
body
}
totalCount
}
reviews(first: 100) {
nodes {
body
}
totalCount
}
}
pageInfo {
endCursor
startCursor
hasNextPage
}
totalCount
}
}
}
}