其實我家老大找到的這篇,就已經完整說明設計RESTful API各種要注意的地方:
http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
以下是個人簡單的筆記:
透過Http協定來提供服務有以下好處:
- Http協定已經是一個成熟穩定的傳輸協定,累積了業界的經驗,而且有許多的Http Server支援,不用自己再打造協定,只須選擇一種Http Server來使用,並把應用程式佈署進去。
- Client端也有許多現成的Open Source實作,不需自行打造穩定可靠的Client端
- 協定都是以文字為主,理解與除錯較簡單
- 至於傳輸內容部份,應用程式相關資料,目前業界都是使用肉眼可讀的JSON,怕字串容量過大,各家http server都有現成的工具可壓縮。
- 安全部份有https可以使用,加密也不需自行處理,可以使用Open Source的Solution
- Http本身也支援Cache機制,善加利用對於處理大量讀取需求也會很有幫助。
REST:Representational State Transfer,含狀態傳輸,來自Roy Fielding博士的論文
是一種軟體架構風格,在目前的Web Service實現方案中,與SOAP,XML-RPC簡單許多,這兩種我光是看就頭暈,所以REST已經變成實現Web Service的主流。科技走向始終來自於人性,一看就覺得違背工程師懶惰天性的規格,根本不需要花太多時間去看。
RESTful Web API,是使用HTTP並遵循REST原則的Web API,比較簡單的定義可以看 http://zh.wikipedia.org/wiki/REST ,
簡單易懂的RESTful CookBook:http://restcookbook.com/
可以看看GitHub怎麼訂的:https://developer.github.com/v3/
Google api explorer:https://developers.google.com/apis-explorer/#p/
Twitter的REST API:https://dev.twitter.com/docs/api/1.1
在把HTTP Method對應到Resource存取方式時:
GET-->讀取Resource
POST-->新增Resource,呼叫多次會新增多個
PATCH-->更新Resource,可作部份更新
PUT-->替換Resource,用新的Resource換掉一個舊的Resource
DELETE-->刪除Resource
另外有一個表格一定要了解:from http://restcookbook.com/
HTTP Method | Idempotent | Safe |
---|---|---|
OPTIONS | yes | yes |
GET | yes | yes |
HEAD | yes | yes |
PUT | yes | no |
POST | no | no |
DELETE | yes | no |
PATCH | no | no |
Idempotent:代表Http Request會改變Server資料內容,但多次Request結果都不會改變。
Safe:代表Http Request不會改變Server資料內容
在設計RESTful API時,處理什麼資源要對應什麼Method,要小心考慮是否需要Idempotent與Safe。例如想更新某筆已經存在的使用者資料,卻用POST就瞎了。雖然還是可以作,但最好不要宣稱是RESTful。
以下是一些設計上的小筆記:
版本直接寫在URI上
只要有API就會有版本問題,早期版本資訊是放在http的request body裡面,但是這樣可讀性並不好,而且這個Request是哪個版本的,要解析Request body才知道。現在比較有彈性的設計是放在URL上,例如:
/v1/team/:id/members
/v1/team/:id/members/:user
到
/v3/team/:id/members
/v3/team/:id/members/:user
當Server升到v3的時候,如果實作相同,可以把v1的Request導到v3,達成Backward Compatible,如果實作不同,就保留v1的實作。
Enable與Disable的設計
要Enable/Disable某項資源時,可以做成HTTP PUT與HTTP DELETE,以下是Github在Repository加Star的例子:
- PUT /user/starred/:owner/:repo:對某個repository加Star
- DELETE /user/starred/:owner/:repo:對某個repository去掉Star
API就是User Story
在定義API時,用User Story來表示API比起用資料存取來表示API更好,因為這樣User Story就浮現,也不會多開放不需要的東西出去。以下是Github對於管理Team的API列表:
- List teams
- Get team
- Create team
- Edit team
- Delete team
- List team members
- Get team member
- Add team member
- Remove team member
- List team repos
- Check if a team manages a repository
- Add team repository
- Remove team repository
- List user teams
可以看到紅色的項目是特殊條件查詢,這樣把一個一個User Story列出來,開發的方式就會從資料導向變成使用者導向,我們只開放使用者需要的功能,寫出User Story就代表我們充分研究過使用者需求,寫出User Story還可進一步作BDD與ATDD,這可是一個軟體工程層次的進化!
以Aggregate為單位操作
Aggregate(你也可以叫做Entity,我在這裡是用DDD的講法)概念的使用,早期Http PATCH不風行,每次要更新資料,都要把完整一包資料準備好,一併Http PUT上去,較耗時間與計算資源,所以一種變通的作法,就是細分出很多URL,來對一個Aggregate作paritial update,例如有一個team Object,JSON如下:
{ "url": "https://api.github.com/teams/1", "name": "Owners", "id": 1, "permission": "admin", "members_count": 3, "repos_count": 10, "organization": { "login": "github", "id": 1, "url": "https://api.github.com/orgs/github", "avatar_url": "https://github.com/images/error/octocat_happy.gif" } }
假設orgnization又臭又長,有1000 char,每次我要更新team的members_count我必需
PUT /teams/:id
這樣每次要傳那個orgnization,消費太大又沒意義,所以可以用
PUT /teams/:id/members_count
這類的URL,只需要傳members_count。
以上作法沒什麼錯,但是這樣URI會增加很多,而且team是一個邏輯上完整的Aggregate,而members_count離開了team,就什麼也不是。以架構的角度來看,API每次給出來的,都應該是完整的資料。唯一需要部份資料更新的情境,應該就是Web UI想要減少傳輸量所以要做partial update,至於partial get,只會讓API提供者與API使用者搞死自己。
現在有Http PATCH有支援partial update,所以更新資料可以用以下形式:
{ "url": "https://api.github.com/teams/1", "name": "Owners", "members_count": 3, }
只要用PATCH /teams/:id就可以了
既然現在PATCH已經成為主流,那更應該保持API給出的資料是個完整的Aggregate,這樣可以讓簡化資料處理邏輯,也可減少URI的數量。
URI不一定要完整表示資料的階層
假設一個資料階層是:School包含Department包含Class包含Student
如果要拿某一個student,可能會設計成
/school/:schoolid/department/:departmentid/class/:classid/student/:studentid
這樣又臭又長的URI,這個作法把資料階層的複雜度直接呈現給使用者,如果用以下URI
/student/:studentid
是不是相對簡單,但是我們怎麼只根據studentid找到學生?這部份就是後台設計的問題,或許可以讓使用者指定schoolid與departmentid與classid在Request Body裡面,或許可以將studentid定義成整個應用程式都不會衝突的UUID,然後直接搜尋這個id,不管如何,設計URI的考量應該以想要怎麼實現User Story,而不是以內部資料階層來侷限User Story。
反過來說,如果User Story需要呈現Class與Student的關係,那也是可以定義成:
/class/:classid/student/:studentid
動詞放在URI的最後面
RESTful的標準用法都是以資源存取為主,但是有許多User Story不只使用資源,而是有一些動作要系統執行,那要用API來呈現呢?
- 把要執行的動作當作一種資源,然後去Patch,壞處是把動作當資源這種觀念上的轉換不太直覺,Ex: PATCH /comuters/:computerid/activated
- 把要執行的動作的目標資源,後面加上sub resource,然後作PUT/DELETE,等同上述的Enable/Disable,這其實也不太直覺,Ex: PUT /computers/:comuterid/active,DELETE /computers/:computerid/active
- 把要執行的動作放在URL的最後面,這個方式打破RESTful架構在URI只放名詞的規範,但是如果把動詞放在URI的最後面,且API從頭到尾保持一致,個人也覺得是最有說明性的方式。https://developers.google.com/prediction/docs/reference/v1.6/,在GET /project/trainedmodels/id/analyze 就是拿專案某id的訓練模型來做....analyze。
Parameter的使用方式
parameter是拿來放filter條件或是Sort條件,state=是filter條件,sort=是sort條件
GET /tickets?state=closed&sort=-updated_at
更複雜的運用就是放Search Criteria
總是使用SSL
使用SSL連線可以免去每個Request都要作認證授權的麻煩,最重要的是安全性!我們無法預期用戶會在什麼地方呼叫我們的API,最好最省時間的方式就是全部都用SSL連線!Https早就有這個基礎設施。雖然最近有個HeartBleed的案件,但是還是應該使用,真的中鏢再積極補救。
總是使用gzip
可以省掉60%的頻寬,而Twitter自己作的研究極端的Case可以省80%的頻寬,另外就是用gzip就不用擔心回傳格式排版,可以排成肉眼方便讀取的格式,也不會佔空間。
Error Code的定義
Http協定本身就已經定義了Error code以及其所代表的意義,規劃RESTful API時一般都是會按照Http協定的定義,來將應用程式的錯誤映射到Http Error Code:
4xx:代表Client傳來的資料有問題,可能是格式錯誤,欄位錯誤等。
5xx:代表Server內部處理時有問題,可能是連不到資料庫,程式處理錯誤等等,Client可以嘗試用同樣的Request再發一次。
把應用程式自己的錯誤碼映射到Http Error Code的理由是符合Client的預期,Client就算不看任何文件,光是看Http Error Code就可以知道是自己錯還是Server錯
至於Error code 500則是有不同意見,一種意見是如果Server有錯,不知道要發什麼就發500。一是完全不要發500,以免有人惡意攻擊。
沒有留言:
張貼留言