硅谷的apigee公司給出一份對REST API的設(shè)計指導(dǎo)原則堪伍,可以說這家公司在api開發(fā),管理的成績有目共睹拆魏。其提供的指導(dǎo)原則气筋,可以說結(jié)合了其自身實際開發(fā)經(jīng)驗,諸多大型平臺的實際運營經(jīng)驗和標(biāo)準(zhǔn)http規(guī)范连锯。非常值得一讀归苍。
首先,你需要對REST API有一個基本的概念認(rèn)知运怖,然后再深入閱讀:
1. 基于業(yè)務(wù)領(lǐng)域的數(shù)據(jù)建模拼弃,而非基于功能建模。
例如摇展,取得所有的dog
GET /api/dogs
取得一個特定的dog
GET /api/dogs/{id}
取得特定名字的dogs
GET /api/dogs/?name=xxx
創(chuàng)建一個dog
POST /api/dogs
更改一個dog
PUT /api/dogs/{id}
刪除一個dog
DELETE /api/dogs/{id}
標(biāo)準(zhǔn)的HTTP的方法已經(jīng)提供了一套約定俗成的操作語義吻氧。
而一個基于功能建模的api,通常會是下面的樣子:
/getAllDogs/getDogsByNames/getAllBabyDogs/createDogs/createThreeDogs/saveDogs
以上這些我經(jīng)常在新手的代碼里看到。這樣做的代碼盯孙,沒錯鲁森,可以運行,但是不‘標(biāo)準(zhǔn)’ why振惰?基于功能建模的api歌溉,首先會造成學(xué)習(xí)曲線的增長,不容易上手报账,也往往意味著需要記憶大量的url(swagger會解決一部分這個問題研底,但不是全部)。
使用HTTP的標(biāo)準(zhǔn)方法作為操作數(shù)據(jù)的基本語義透罢,勝在其標(biāo)準(zhǔn)的普適性榜晦。這一點上,大的平臺Github羽圃,Heroku等乾胶,做的是最好的。
2. 設(shè)計數(shù)據(jù)的表現(xiàn)形式
毫無疑問朽寞,使用JSON识窿,今天JSON已經(jīng)是事實上的web數(shù)據(jù)標(biāo)準(zhǔn),簡單易懂脑融。但是JSON也有其缺點喻频,比如如何表達Date和Timestamp,一個簡單的做法是用string來表達肘迎,根據(jù)上下文來判斷如何解釋甥温。盡量保持JSON數(shù)據(jù)的簡潔性。
并且使用link去表達資源之間的聯(lián)系:
例如:
{"id": "12345678","kind": "Dog""name": "Lassie","furColor": "brown","ownerID": "98765432"}
更好的方法:
{"id": "12345678","kind": "Dog""name": "Lassie","furColor": "brown","ownerID": "98765432","ownerLink": "https://dogtracker.com/persons/98765432"}
更簡潔的表示:
{"id": "12345678","kind": "Dog""name": "Lassie","furColor": "brown","owner": "https://dogtracker.com/persons/98765432"}
這樣做的好處是客戶端不需要自己重新構(gòu)建URL去獲取owner妓布,更方便使用姻蚓。Google Drive API 和 Github 均使用這種方式。但這樣做也不是沒有壞處匣沼,最容易想到的是狰挡,如果url改變了怎么辦?production释涛,staging加叁,testing用的是不一樣的domain喲。一個方法是:使用相對路徑唇撬,而不是絕對路徑殉农。但這也不能解決全部問題。
3. 設(shè)計URL的表現(xiàn)形式
兩大原則:規(guī)范的(regular)局荚,可預(yù)測的(predictable)超凳。
只使用名詞
要有切入點
要合適的選擇id形式
要表達資源間的聯(lián)系
要支持查詢
要支持返回部分資源
處理更復(fù)雜的計算邏輯
只使用名詞:在URL中使用名詞愈污,避免動詞,一旦使用動詞轮傍,意味著你是在對功能建模暂雹,而非數(shù)據(jù)。
要有切入點:原則上來講创夜,一個api應(yīng)該有一個root path '/', 其返回一個url map杭跪,包括了所有的resouces所對應(yīng)的url。這樣客戶端更容易去發(fā)現(xiàn)和使用api驰吓。
要合適的選擇id形式:例如api/dogs/{id} 中的{id}如何表達涧尿,是類似于‘/dogs/1’,還是‘/dogs/haha’? 一般來說取決于后端的數(shù)據(jù)庫檬贰,大多數(shù)情況下姑廉,使用RDBMS,像mysql之類的主鍵自增功能翁涤,我比較傾向于使用自增主鍵的整數(shù)直接作為entity的id桥言,避免很多問題,如果使用MongoDB的話葵礼,不妨試一試用字符串作為entity的id号阿,可讀性會提高,但是如何維護一個全局唯一的字符主鍵鸳粉,你得三思扔涧。
要表達資源間的聯(lián)系:比如 GET /persons/1/dogs 返回所有屬于person 1的狗。
這種模式可以表述為:
/{relationship-name}[/{resource-id}]/…/{relationship-name}[/{resource-id}]
要支持查詢:
GET /persons;1/dogs GET /persons;name=blabla/dogs
這種模式可以表述為:
/{relationship-name}[;{selector}]/…/{relationship-name}[;{selector}]
更復(fù)雜的查詢條件:
GET /dogs?color=red&state=running&location=park
注意這里的三個查詢子條件之間的關(guān)系是‘與(and)’,如果要表達是‘或(or)’的邏輯届谈,那就得設(shè)計更復(fù)雜的query解釋機制扰柠。但其實實際使用中,表達‘或’的查詢條件很少使用疼约。
要支持返回部分資源:
/dogs?fields=name,color,location
返回的resource中,只包含name,color,location三種信息蝙泼。
處理更復(fù)雜的計算邏輯: 有很多例子程剥,比如貨幣轉(zhuǎn)換,大多數(shù)開發(fā)人員給出的方案是:
/convert/100/EUR/CNY 或者 /convert?quantity=100&unit=EUR&in=CNY
切記:URL是用來表述資源resource汤踏,而不是表述計算的過程织鲸。在URL中使用名詞,避免動詞溪胶。
改進的方案1:
GET /monetary-amount/100/EUR HTTP/1.1Host: Currency-Converter.comAccept-Currency:CNY //在http的header中添加Accept-Currency來指明貨幣的種類搂擦。
改進的方案2:
POST /currency-converter HTTP/1.1Host: Currency-Converter.comContent-Length: 69{"amount": 100,"inputCurrency": "EUR","outputCurrency": "CNY"}
兩個方案都可行,但是方案2有兩個注意的地方:POST返回的結(jié)果可能無法再server端緩存;你是在構(gòu)建一個計算的過程哗脖,而非資源的表述瀑踢,如何理解扳还?就像數(shù)據(jù)庫操作中的‘store procedure’,你可以使用橱夭,并且功能強大氨距,但是接口變得復(fù)雜,邏輯變得耦合棘劣。
4. 反思-設(shè)計數(shù)據(jù)的表現(xiàn)形式俏让。
添加self link
集合數(shù)據(jù)
數(shù)據(jù)分頁
數(shù)據(jù)格式
添加self link:self link提供了一個上下文環(huán)境,客戶端可以更容易理解當(dāng)前的resource的位置
{"user": { "html_url": "octocat (The Octocat)", "type": "User", "url": "https://api.github.com/users/octocat"}}
集合數(shù)據(jù):
方案1:集合collection也是一種resource茬暇,也具有self和kind屬性首昔,這樣所有的單獨entity和collection都具有更加統(tǒng)一的規(guī)范
{"self": "https://dogtracker.com/dogs","kind": "Collection","contents": [{"self": "https://dogtracker.com/dogs/12344","kind": "Dog","name": "Fido","furColor": "white"},{"self": "https://dogtracker.com/dogs/12345","kind": "Dog","name": "Rover","furColor": "brown"}]}
方案2:看起來更加簡潔,但是客戶端可能需要去添加額外的邏輯去處理collection
[{"self": "https://dogtracker.com/dogs/12344","kind": "Dog","name": "Fido","furColor": "white"},{"self": "https://dogtracker.com/dogs/12345","kind": "Dog","name": "Rover","furColor": "brown"}]
還有一種做法是糙俗,針對一個collection勒奇,使用自定義的media type header(比如‘Collection+JSON’)這個方法可行,但是會讓客戶端的處理邏輯復(fù)雜臼节。
數(shù)據(jù)分頁:當(dāng)數(shù)據(jù)返回的集合變大時撬陵,顯然不可能一次性把所有數(shù)據(jù)都返回給客戶端,最好能分批的返回网缝,比如:
GET https://dogtracker.com/dogs?limit=25,offset=0返回{"self": "https://dogtracker.com/dogs?limit=25,offset=0","kind": "Page","pageOf": "https://dogtracker.com/dogs","next": "https://dogtracker.com/dogs?limit=25,offset=25","contents": [...】}
在返回的數(shù)據(jù)中巨税,加入‘pageOf’來指明查詢的起點,‘next’指明下一頁的url粉臊,當(dāng)返回第二頁的時候草添,還需加入‘previous’來指明上一頁。
數(shù)據(jù)格式:現(xiàn)在大多數(shù)的api幾乎只支持JOSN格式的數(shù)據(jù)來作為input和output扼仲,如果要支持更多的數(shù)據(jù)格式远寸,那么應(yīng)該要支持Http Accept Header。
能否用HTML作為輸出的格式屠凶?可以驰后,但是這樣就喪失的rest API的靈活性。現(xiàn)代的web應(yīng)用矗愧,大多使用REST API + SPA的設(shè)計灶芝,SPA端使用Angular等框架,自己渲染HTML唉韭,REST API只提供數(shù)據(jù)服務(wù)夜涕,前端后端通過JSON數(shù)據(jù)來交流,從而實現(xiàn)了前后端的徹底解耦属愤。
如果選擇JSON作為唯一的數(shù)據(jù)格式女器,那么最好支持Http的patch方法,現(xiàn)在有兩種patch的模式:JSON Patch和JSON Merge Patch住诸,選擇一個來用于資源的更新操作〖莸ǎ現(xiàn)在也有很多API只提供PUT來更新資源涣澡,這意味著每次請求都必須發(fā)送整個resource enrity,勢必會消耗更多的payload俏拱,但是實現(xiàn)起來更容易暑塑。
5. 錯誤處理
總的原則:使用標(biāo)準(zhǔn)http的status code來表示錯誤的類型。具體的錯誤內(nèi)容锅必,也要被返回事格。
6. 認(rèn)證和授權(quán)
人生苦短,使用OAuth2搞隐。最起碼也要使用基于token的鑒權(quán)模式驹愚。
7. SDK
可以推出SDK來作為你的REST API的一個補充,就像AWS那樣劣纲,針對每一個服務(wù)逢捺,都有相應(yīng)的編程語言的SDK。這樣更方便第三方的開發(fā)人員使用你的api癞季。多見于SaaS平臺劫瞳。但是小型的平臺,得考慮維護的成本绷柒。
8. Versioning 多版本
REST API的版本控制問題是一個非常有爭議的話題志于,網(wǎng)上的提議有很多,在這里我們不是簡單的給定具體的方法废睦,而是提供幾種可行的想法伺绽,具體的實施還需自己拿捏:
不(顯式)支持多版本
使用Http Accept Header
第一種,什么都不做嗜湃,不支持多版本的api奈应。這個想法的背后依據(jù)是,根據(jù)調(diào)研發(fā)現(xiàn)购披,大多數(shù)的中小型規(guī)模的平臺服務(wù)杖挣,客戶規(guī)模都在一個可控的范圍,api的升級不會很頻繁刚陡,你只需通知你的客戶惩妇,在某個時間點api會更新,然后再server端做一些兼容性的數(shù)據(jù)遷移橘荠,比如增加或刪除某個數(shù)據(jù)庫中的表的某個列的名字。大多數(shù)情況下郎逃,支持多版本api費力不討好哥童,測試和部署的成本很大,收益卻很小褒翰。你要做就是保持唯一個可用api服務(wù)的兼容性贮懈,而不是提供多個版本的api讓用戶使用匀泊。
第二種,如果你一定要支持versioning朵你,那么就在http的accept header中添加version信息各聘,不要在url中使用version信息,千萬不要用/api/v1/xxx抡医。
實際的工作中躲因,對于剛?cè)肼毜男∨栌眩诮榻BREST API的時候忌傻,我會推薦他們讀這個大脉。
具體的可以參考:Zalando RESTful API and Event Scheme Guidelines其中的規(guī)范涵蓋了很多細節(jié)內(nèi)容,細節(jié)的規(guī)范越多水孩,代碼風(fēng)格才越統(tǒng)一镰矿,團隊溝通效率才越高。