來源:http://www.oschina.net/translate/best-practices-for-a-pragmatic-restful-apihtml
數據模型已經穩定,接下來你可能須要爲web(網站)應用建立一個公開的API(應用程序編程接口)。須要認識到這樣一個問題:一旦API發佈後,就很難對它作很大的改動而且保持像先前同樣的正確性。如今,網絡上有不少關於API設計的思路。可是在所有案例中沒有一種被普遍採納的標準,有不少的選擇:你接受什麼樣的格式?如何認證?API應該被版本化嗎?python
在爲SupportFu(一個輕量級的Zendesk替換實現)設計API時,對於這些問題我儘可能得出一些務實的答案。個人目標是設計這樣一個API,它容易使用和採納,足夠靈活去爲咱們用戶接口去埋單。git
許多網上能找到的API設計觀點都是些學術討論,這些討論是關於模糊標準的主觀解釋,而不是關於在現實世界中具備意義的事。本文中個人目標是,描述一下爲當今的web應用而設計的實用的API的最佳實踐。若是感受不對,我不會去嘗試知足某個標準。爲了幫助進行決策,我已經寫下了API必須力爭知足的一些要求:github
一個 API 是一個開發者的 UI - 就像其餘任何 UI 同樣, 確保用戶體驗被認真的考慮過是很重要的!web
若是有同樣東西得到普遍承認的話,那就是 RESTful 原則。Roy Felding 在他論文 network based software architectures 的 第五章 中首次介紹了這些原則。apache
這些REST的關鍵原則與將你的 API 分割成邏輯資源緊密相關。使用HTTP請求控制這些資源,其中,這些方法(GET, POST, PUT, PATCH, DELETE)具備特殊含義。編程
但是我該整出什麼樣的資源呢?好吧,它們應該是有意義於 API 使用者的名詞(不是動詞)。雖然內部Model能夠簡單地映射到資源上,但那不必定是個一對一的映射。這裏的關鍵是不要泄漏與API不相關的實現細節。一些相關的名詞能夠是票,用戶和小組。json
一旦定義好了資源, 須要肯定什麼樣的 actions 應用它們,這些 actions 怎麼映射到你的 API 上。RESTful 原則提供了 HTTP methods 映射做爲策略來處理 CRUD actions,以下:api
REST 很是棒的是,利用現有的 HTTP 方法在單個的 /tickets 接入點上實現了顯著的功能。沒有什麼方法命名約定須要去遵循,URL 結構是整潔乾淨的。 REST 太棒了!跨域
接入點的名稱應該選擇單數仍是複數呢?keep-it-simple原則能夠在此應用。雖然你內在的語法知識會告訴你用複數形式描述單一資源實例是錯誤的,但實用主義的答案是保持URL格式一致而且始終使用複數形式。不用處理各類奇形怪狀的複數形式(好比person/people,goose/geese)可讓API消費者的生活更加美好,也讓API提供者更容易實現API(由於大多數現代框架自然地將/tickets和/tickets/12放在同一個控制器下處理)。
可是你該如何處理(資源的)關係呢?若是關係依託於另一個資源,Restful原則提供了很好的指導原則。讓咱們來看一個例子。SupportFu的一個ticket包含許多消息(message)。這些消息邏輯上與/tickets接入點的映射關係以下:
或者若是某種關係不依賴於資源,那麼在資源的輸出表示中只包含一個標識符是有意義的。API消費者而後除了請求資源所在的接入點外,還得再請求一次關係所在的接入點。可是若是通常狀況關係和資源一塊兒被請求,API能夠提供自動嵌套關係表示到資源表示中,這樣能夠防止兩次請求API。
若是Action不符合CRUD操做那該怎麼辦?
這是一個可能讓人感到模糊不解的地方。有幾種處理方法:
老是使用SSL,沒有例外。今天,您的web api能夠從任何地方訪問互聯網(如圖書館、咖啡店、機場等)。不是全部這些都是安全的,許多不加密通訊,便於竊聽或僞造,若是身份驗證憑證被劫持。
另外一個優勢是,保證老是使用SSL加密通訊簡化了認證效果——你能夠擺脫簡單的訪問令牌,而不是讓每一個API請求籤署。
要注意的一點是非SSL訪問API URLs。不要重定向這些到對應的SSL。相反,拋出一個系統錯誤!最後一件你想要的是配置不佳的客戶發送請求到一個未加密的端點,只是默默地重定向到實際加密的端點
API的好壞關鍵看其文檔的好壞. 好的API的說明文檔應該很容易就被找到,並能公開訪問。在嘗試任何整合工做前大部分開發者會先查看其文檔。當文檔被藏於一個PDF之中或要求必須登記信息時,將很難被找到也很難搜索到。
好的文檔須提供從請求到響應整個循環的示例。最好的是,請求應該是可粘貼的例子,要麼是能夠貼到瀏覽器的連接,要麼是能夠貼到終端裏的curl示例 。 GitHub 和 Stripe 在這方面作的很是出色。
一旦你發佈一個公開的API,你必須承諾"在沒有通告的前提下,不會更改APIDe功能" .對於外部可見API的更新,文檔必須包含任何將廢棄的API的時間表和詳情。應該經過博客(更新日誌)或者郵件列表送達更新說明(最好二者都通知)。
必須對API進行版本控制。版本控制能夠快速迭代並避免無效的請求訪問已更新的接入點。它也有助於幫助平滑過渡任何大範圍的API版本變遷,這樣就能夠繼續支持舊版本API。
關於API的版本是否應該包含在URL或者請求頭中 莫衷一是。從學術派的角度來說,它應該出如今請求頭中。然而版本信息出如今URL中必須保證不一樣版本資源的瀏覽器可瀏覽性(browser explorability),還記得文章開始提到的API要求嗎?
我很是同意 approach that Stripe has taken to API versioning - URL包含一個主版本號(好比http://shonzilla/api/v1/customers/1234)
),可是API還包含基於日期的子版本(好比http://shonzilla/api/v1.2/customers/1234),能夠經過配置HTTP請求頭來進行選擇。這種狀況下,主版本確保API結構整體穩定性,而子版本會考慮細微的變化(field deprecation、接入點變化等)。
API不可能徹底穩定。變動不可避免,重要的是變動是如何被控制的。維護良好的文檔、公佈將來數月的deprecation計劃,這些對於不少API來講都是一些可行的舉措。它歸根結底是看對於業界和API的潛在消費者是否合理。
最好是儘可能保持基本資源URL的簡潔性。 複雜結果過濾器、排序需求和高級搜索 (當限定在單一類型的資源時) ,都可以做爲在基本URL之上的查詢參數來輕鬆實現。下面讓咱們更詳細的看一下:
過濾: 對每個字段使用一個惟一查詢參數,就能夠實現過濾。 例如,當經過「/tickets」終端來請求一個票據列表時,你可能想要限定只要那些在售的票。這能夠經過一個像 GET /tickets?state=open 這樣的請求來實現。這裏「state」是一個實現了過濾功能的查詢參數。
排序: 跟過濾相似, 一個泛型參數排序能夠被用來描述排序的規則. 爲適應複雜排序需求,讓排序參數採起逗號分隔的字段列表的形式,每個字段前均可能有一個負號來表示按降序排序。咱們看幾個例子:
搜索: 有時基本的過濾不能知足需求,這時你就須要全文檢索的力量。或許你已經在使用 ElasticSearch 或者其它基於 Lucene 的搜索技術。當全文檢索被用做獲取某種特定資源的資源實例的機制時, 它能夠被暴露在API中,做爲資源終端的查詢參數,咱們叫它「q」。搜索類查詢應當被直接交給搜索引擎,而且API的產出物應當具備一樣的格式,以一個普通列表做爲結果。
把這些組合在一塊兒,咱們能夠建立如下一些查詢:
通常查詢的別名
爲了使普通用戶的API使用體驗更加愉快, 考慮把條件集合包裝進容易訪問的RESTful 路徑中。好比上面的,最近關閉的票的查詢能夠被包裝成 GET /tickets/recently_closed
API的使用者並不老是須要一個資源的完整表示。選擇返回字段的功能由來已久,它使得API使用者可以最小化網絡阻塞,並加速他們對API的調用。
使用一個字段查詢參數,它包含一個用逗號隔開的字段列表。例如,下列請求得到的信息將剛剛足夠展現一個在售票的有序列表:
GET /tickets?fields=id,subject,customer_name,updated_at&state=open&sort=-updated_at
一個 PUT, POST 或者 PATCH 調用可能會對指定資源的某些字段形成更改,而這些字段本不在提供的參數之列 (例如: created_at 或 updated_at 這兩個時間戳)。 爲了防止API使用者爲了獲取更新後的資源而再次調用該API,應當使API把更新(或建立)後的資源做爲response的一部分來返回。
以一個產生建立活動的 POST 操做爲例, 使用一個 HTTP 201 狀態代碼 而後包含一個 Location header 來指向新生資源的URL。
你是否應該HATEOAS?(譯註:Hypermedia as the Engine of Application State (HATEOAS)超媒體做爲應用程序狀態引擎) 對於API消費方是否應該建立連接,或者是否應該將連接提供給API,有許多混雜的觀點。RESTful的設計原則指定了HATEOAS ,大體說明了與某個端點的交互應該定義在元數據(metadata)之中,這個元數據與輸出結果一同到達,並不基於其餘地方的信息。 雖然web逐漸依照HATEOAS類型的原則運做(咱們打開一個網站首頁並隨着咱們看到的頁面中的連接瀏覽),我不認爲咱們已經準備好API的HATEOAS了。當瀏覽一個網站的時候,決定點擊哪一個連接是運行時作出的。然而,對於API,決定哪一個請求被髮送是在寫API集成代碼時作出的,並非運行時。這個決定能夠移交到運行時嗎?固然能夠,不過順着這條路沒有太多好處,由於代碼仍然不能不中斷的處理重大的API變化。也就是說,我認爲HATEOAS作出了承諾,可是尚未準備好迎接它的黃金時間。爲了徹底實現它的潛能,須要付出更多的努力去定義圍繞着這些原則的標準和工具。 目前而言,最好假定用戶已經訪問過輸出結果中的文檔&包含資源標識符,而這些API消費方會在製做連接的時候用到。關注標識符有幾個優點——網絡中的數據流減小了,API消費方存儲的數據也減小了(由於它們存儲的是小的標識符而不是包含標識符的URLs)。 一樣的,在URL中提供本文倡導的版本號,對於在一個很長時間內API消費方存儲資源標識符(而不是URLs),它更有意義。總之,標識符相對版本是穩定的,可是表示這一點的URL卻不是的! |
是時候在API中丟棄XML了。XML冗長,難以解析,很難讀,他的數據模型和大部分編程語言的數據模型 不兼容,而他的可擴展性優點在你的主要需求是必須序列化一個內部數據進行輸出展現時變得不相干。
我不打算對上述進行解釋了,貌似諸如 (YouTube, Twitter 和 Box)之類的已經開始了去XML化.
給你一張google趨勢圖,比較XML API 和 JSON API的,供你參考:
可是,若是你的客戶羣包括大量的企業客戶,你會發現本身不得不支持XML的方式。若是你必須這樣,一個新問題出現了:
媒體類型是應該基於Accept頭仍是基於URL呢 ? 爲確保瀏覽器的瀏覽性,應該基於URL。這裏最明智的選擇是在端點URL後面附加 .json 或 .xml 的擴展.
若是你在使用JSON (JavaScript Object Notation) 做爲你的主要表示格式,正確的方法就是遵照JavaScript命名約定——對字段名稱使用camelCase!若是你要走用各類語言建設客戶端庫的路線,最好使用它們慣用的命名約定—— C# & Java 使用camelCase, python & ruby 使用snake_case。
深思:我一直認爲snake_case比JavaScript的camelCase約定更容易閱讀。我沒有任何證據來支持個人直覺,直到如今,基於從2010年的camelCase 和 snake_case的眼動追蹤研究 (PDF),snake_case比駝峯更容易閱讀20%!這種閱讀上的影響會影響API的可勘探性和文檔中的示例。
許多流行的JSON API使用snake_case。我懷疑這是因爲序列化庫聽從它們所使用的底層語言的命名約定。也許咱們須要有JSON序列庫來處理命名約定轉換。
缺省狀況下確保漂亮的打印和支持gzip一個提供空白符壓縮輸出的API,從瀏覽器中查看結果並不美觀。雖然一些有序的查詢參數(如 ?pretty=true )能夠提供來使漂亮打印生效,一個默認狀況下能進行漂亮打印的API更爲平易近人。額外數據傳輸的成本是微不足道的,尤爲是當你比較不執行gzip壓縮的成本。 考慮一些用例:假設分析一個API消費者正在調試而且有本身的代碼來打印出從API收到的數據——默認狀況下這應是可讀的。或者,若是消費者抓住他們的代碼生成的URL,並直接從瀏覽器訪問它——默認狀況下這應是可讀的。這些都是小事情。作好小事情會使一個API能被更愉快地使用!
|
那麼該如何處理額外傳輸的數據呢?
讓咱們看一個實際例子。我從GitHub API上拉取了一些數據,默認這些數據使用了漂亮打印(pretty print)。我也將作一些GZIP壓縮後的對比。
1 |
$ curl https://api.github.com/ users /veesahni > with-whitespace.txt |
2 |
$ ruby -r json -e 'puts JSON JSON.parse(STDIN.read)' < with-whitespace.txt > without-whitespace.txt |
3 |
$ gzip -c with-whitespace.txt > with-whitespace.txt.gz |
4 |
$ gzip -c without-whitespace.txt ? without-whitespace.txt.gz |
輸出文件的大小以下:
在這個例子中,當未啓用GZIP壓縮時空格增長了8.5%的額外輸出大小,而當啓用GZIP壓縮時這個比例是2.6%。另外一方面,GZIP壓縮節省了60%的帶寬。因爲漂亮打印的代價相對比較小,最好默認使用漂亮打印,並確保GZIP壓縮被支持。
關於這點想了解更多的話,Twitter發現當對他們的 Streaming API 開啓GZIP支持後能夠在某些狀況得到 80%的帶寬節省 。Stack Exchange甚至強制要求必須對API請求結果使用GZIP壓縮(never return a response that's not compressed)。
許多API會像下面這樣包裹他們的響應信息:
1 |
{ |
2 |
"data" : { |
3 |
"id" : 123, |
4 |
"name" : "John" |
5 |
} |
6 |
} |
有很多這樣作的理由 - 更容易附加元數據或者分頁信息,一些REST客戶端不容許輕易的訪問HTTP頭信息,而且JSONP請求不能訪問HTTP頭信息。不管怎樣,隨着迅速被採用的標準,好比CORS和Link header from RFC 5988, 大括號封裝開始變得沒必要要。
咱們應當默認不使用大括號封裝,而僅在特殊狀況下使用它,從而使咱們的API面向將來。
特殊狀況下該如何使用大括號封裝?
有兩種狀況確實須要大括號封裝 - 當API須要經過JSONP來支持跨域的請求時,或者當客戶端沒有能力處理HTTP頭信息時。
JSONP 請求附帶有一個額外的查詢參數(一般稱爲callback或jsonp) 表示了回調函數的名稱。若是提供了這個參數,API應當切換至完整封裝模式,這時它老是用200HTTP狀態碼做爲響應,而後把真實的狀態碼放入JSON有效載荷中。任何被一併添加進響應中的額外的HTTP頭信息都應當被映射到JSON字段中, 像這樣:
1 |
callback_function({ |
2 |
status_code: 200, |
3 |
next_page: "https://.." , |
4 |
response: { |
5 |
... actual JSON response body ... |
6 |
} |
7 |
}) |
相似的,爲了支持HTTP受限的客戶端,能夠容許一個特殊的查詢參數「?envelope=true」來觸發完整封裝(沒有JSONP回調函數)。
若是你正在跟隨本文中講述的開發過程,那麼你確定已經接受JSON做爲API的輸出。下面讓咱們考慮使用JSON做爲API的輸入。
許多API在他們的API請求體中使用URL編碼。URL編碼正如它們聽起來那樣 - 將使用和編碼URL查詢參數時同樣的約定,對請求體中的鍵值對進行編碼。這很簡單,被普遍支持並且實用。
然而,有幾個問題使得URL編碼不太好用。首先,它沒有數據類型的概念。這迫使API從字符串中轉換整數和布爾值。並且,它並無真正的層次結構的概念。儘管有一些約定,能夠用鍵值對構造出一些結構(好比給一個鍵增長「[]」來表示一個數組),但仍是不能跟JSON原生的層次結構相比。
若是API很簡單,URL編碼能夠知足須要。然而,複雜API應當嚴格對待他們的JSON格式的輸入。不論哪一種方式,選定一個而且整套API要保持一致。
一個能接受JSON編碼的POST, PUT 和 PATCH請求的API,應當也須要把Content-Type頭信息設置爲application/json,或者拋出一個415不支持的媒體類型(Unsupported Media Type)的HTTP狀態碼。
信封喜歡將分頁信息包含在信封自身的API。我不能指責這點——直到最近,咱們才找到更好的方法。正確的方法是使用RFC 5988 中介紹的連接標頭。
使用連接標頭的API能夠返回一系列線程的連接,API使用者無需自行生成連接。這在分頁時指針導向 很是重要。下面是抓取自 Github的正確使用連接標頭的文件:
Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next", <https://api.github.com/user/repos?page=50&per_page=100>; rel="last"
不過這個並不是完成版本,由於不少 API 喜歡返回額外信息,例如可用結果的總數。須要發送數量的 API 可用相似 X-Total-Count 的普通 HTTP 標頭。
在不少種狀況下,API的使用者須要加載和被請求資源相關的數據(或被請求資源引用的數據)。與要求使用者反覆訪問API來獲取這些信息相比,容許在請求原始資源的同時一併返回和裝載相關資源,將會帶來明顯的效率提高。
然而, 因爲這樣確實 有悖於一些RESTful原則, 因此咱們能夠只使用一個內置的(或擴展)的查詢參數來實現這一功能,來最小化與原則的背離。
這種狀況下,「embed」將是一個逗號隔開的須要被內置的字段列表。點號能夠用來表示子字段。例如:
GET /ticket/12?embed=customer.name,assigned_user
這將返回一個附帶有詳細內置信息的票據,以下:
01 |
{ |
02 |
"id" : 12, |
03 |
"subject" : "I have a question!" , |
04 |
"summary" : "Hi, ...." , |
05 |
"customer" : { |
06 |
"name" : "Bob" |
07 |
}, |
08 |
assigned_user: { |
09 |
"id" : 42, |
10 |
"name" : "Jim" , |
11 |
} |
12 |
} |
固然,實現相似於這種功能的能力徹底依賴於內在的複雜度。這種內置的作法很容易產生 N+1 select 問題。
一些HTTP客戶端僅能處理簡單的的GET和POST請求,爲照顧這些功能有限的客戶端,API須要一種方式來重寫HTTP方法. 儘管沒有一些硬性標準來作這事,但流行的慣例是接受一種叫 X-HTTP的請求頭,重寫是用一個字符串值包含PUT,PATCH或DELETE中的一個。
注意重寫頭應當僅接受POST請求,GET請求毫不應該 更改服務器上的數據!
爲了防止濫用,標準的作法是給API增長某種類型的速率限制。RFC 6585 中介紹了一個HTTP狀態碼429 請求過多來實現這一點。
不論怎樣,在用戶實際受到限制以前告知他們限制的存在是頗有用的。這是一個如今還缺少標準的領域,可是已經有了一些流行的使用HTTP響應頭信息的慣用方法。
最少時包含下列頭信息(使用Twitter的命名約定 來做爲頭信息,一般沒有中間詞的大寫):
爲何對X-Rate-Limit-Reset不使用時間戳而使用秒數?
一個時間戳包含了各類各樣的信息,好比日期和時區,但它們卻不是必需的。一個API使用者其實只是想知道何時能再次發起請求,對他們來講一個秒數用最小的額外處理回答了這個問題。同時規避了時鐘誤差的問題。
有些API給X-Rate-Limit-Reset使用UNIX時間戳(紀元以來的秒數)。不要這樣作!
爲何對X-Rate-Limit-Reset使用UNIX時間戳是很差的作法?
HTTP 規範已經指定使用RFC 1123 的日期格式 (目前被使用在日期, If-Modified-Since & Last-Modified HTTP頭信息中)。若是咱們打算指定一種使用某種形式時間戳的、新的HTTP頭信息,咱們應當遵循RFC 1123規定,而不是使用UNIX時間戳。
一個 RESTful API 應當是無狀態的。這意味着認證請求應當不依賴於cookie或session。相反,每個請求都應當攜帶某種類型的認證憑證。
因爲老是使用SSL,認證憑證可以被簡化爲一個隨機產生的訪問令牌,裏面傳入一個使用HTTP Basic Auth的用戶名字段。這樣作的極大的好處是,它是徹底的瀏覽器可探測的 - 若是瀏覽器從服務器收到一個401未受權狀態碼,它僅須要一個彈出框來索要憑證便可。
然而,這種基於基本認證的令牌的認證方法,僅在知足下列情形時纔可用,即用戶能夠把令牌從一個管理接口複製到API使用者環境。當這種情形不能成立時,應當使用OAuth 2來產生安全令牌並傳遞給第三方。OAuth 2使用了承載令牌(Bearer tokens) 而且依賴於SSL的底層傳輸加密。
一個須要支持JSONP的API將須要第三種認證方法,由於JSONP請求不能發送HTTP基本認證憑據(HTTP Basic Auth)或承載令牌(Bearer tokens) 。這種狀況下,可使用一個特殊的查詢參數access_token。注意,使用查詢參數token存在着一個固有的安全問題,即大多數的web服務器都會把查詢參數記錄到服務日誌中。
這是值得的,全部上面三種方法都只是跨API邊界兩端的傳遞令牌的方式。實際的底層令牌自己可能都是相同的。
HTTP 提供了一套內置的緩存框架! 全部你必須作的是,包含一些額外的出站響應頭信息,而且在收到一些入站請求頭信息時作一點兒校驗工做。
有兩種方式: ETag和Last-Modified
ETag: 當產生一個請求時, 包含一個HTTP 頭,ETag會在裏面置入一個和表達內容對應的哈希值或校驗值。這個值應當跟隨表達內容的變化而變化。如今,若是一個入站HTTP請求包含了一個If-None-Match頭和一個匹配的ETag值,API應當返回一個304未修改狀態碼,而不是返回請求的資源。
Last-Modified: 基本上像ETag那樣工做,不一樣的是它使用時間戳。在響應頭中,Last-Modified包含了一個RFC 1123格式的時間戳,它使用If-Modified-Since來進行驗證。注意,HTTP規範已經有了 3 種不一樣的可接受的日期格式 ,服務器應當準備好接收其中的任何一種。
就像一個HTML錯誤頁面給訪問者展現了有用的錯誤信息同樣,一個API應當以一種已知的可以使用的格式來提供有用的錯誤信息。 錯誤的表示形式應當和其它任何資源沒有區別,只是有一套本身的字段。
API應當老是返回有意義的HTTP狀態代碼。API錯誤一般被分紅兩種類型: 表明客戶端問題的400系列狀態碼和表明服務器問題的500系列狀態碼。最簡狀況下,API應當把便於使用的JSON格式做爲400系列錯誤的標準化表示。若是可能(意思是,若是負載均衡和反向代理能建立自定義的錯誤實體), 這也適用於500系列錯誤代碼。
一個JSON格式的錯誤信息體應當爲開發者提供幾樣東西 - 一個有用的錯誤信息,一個惟一的錯誤代碼 (可以用來在文檔中查詢詳細的錯誤信息) 和可能的詳細描述。這樣一個JSON格式的輸出可能會像下面這樣:
對PUT, PATCH和POST請求進行錯誤驗證將須要一個字段分解。下面多是最好的模式:使用一個固定的頂層錯誤代碼來驗證錯誤,並在額外的字段中提供詳細錯誤信息,就像這樣:
|
HTTP定義了一套能夠從API返回的有意義的狀態代碼。 這些代碼可以用來幫助API使用者對不一樣的響應作出相應處理。我已經把你必然會用到的那些列成了一個簡短的清單:
一個API是一個給開發者使用的用戶接口。要努力確保它不只功能上可用,更要用起來愉快。