傳說 C 語言風格(C#/Java/JavaScript…)程序員依其信仰分為兩大派,自古以來不共戴天:
依我的觀點,寫 WebAPI 的程序員也分成兩派,RESTful 派跟非 REST 派。我屬於後者,是非主流的少數派。
前幾天跟同事聊到 Web API 是否一定要 RESTful,三言兩語說不清,寫篇文章梳理思緒好了。
RESTful API 是指實踐 REST Representational State Transfer精神的 API 設計風格,其核心精神在於借用 HTTP 協定做為基礎,讓 API 規格簡單一致,大致有以下特色 :
- 透過 URI 指定要存取或操作的資源
- 可使用 QueryString,但只應拿來傳遞額外過濾條件或參數,不應包含識別資源的鍵值
- 使用 HTTP 方法 POST、GET、PUT、DELETE 對應到建立、讀取、更新、刪除等動作。
也有人主張 PUT 是 Relace (Create 或 Update),另外增加 PATCH 用於部分更新( Partial Update ) - 透過 Accept Header 指明可接收的內容格式,例如:XML 或是 JSON
- 伺服器透過 HTTP 狀態碼回傳執行結果,例如:200 成功、401 存取被拒、404 找不到資源、500 伺服器錯誤
而 REST 概念的提出者 Roy Fielding是 HTTP 規範的主要作者及 Apache HTTP Server 專案的發起人之一,這也是讓 RESTful API 風格備受推崇的原因之一。
延伸閱讀
- 程式設計 - 簡明RESTful API設計要點 – Twincl
- RESTful API 設計準則與實務經驗 - Soul & Shell Blog
- [不是工程師] 休息(REST)式架構- 寧靜式(RESTful)的Web API是現在的潮流?
- RESTful探索1-RESTful Web Service on ASP.NET 3.5 計劃 - 黑暗執行緒
採用 RESTful API 最大的好處是風格統一,API 名稱簡潔(不會冒出一堆 QueryThese、UpdateThat、DeleteBlah,動詞隱藏在 HTTP Method ),靠直覺及經驗就能快速上手;除錯時也可由 URL、HTTP 方法及傳回狀態直接解析各項操作的意義及結果。多年來,RESTful 設計已是 Web API 設計的主流,例如:ASP.NET MVC Web API 即是走 RESTful 風格,當在專案新增繼承 ApiController 的 API 類別,預設需實作 Get()、Post()、Put()、Delete() 方法以對映 HTTP GET、POST、PUT、DELETE 等動作。(註:當然,你也可以額外定義路由或使用 [Route("actionName")] 與 [HttpPost] Attribute 加入自訂方法,但要小心濫用會違反 RESTful 精神) ApiController 實做範例可參考:建置使用 ASP.NET Web API 的 RESTful Api - Microsoft Docs
RESFful API 是當今主流,伴隨而來的好處是相關資源豐富(Visual Studio 直接支援,還有自動為 REST API 產生說明文件及測試程式的 Swagger等),無疑是好物,但不幸地,我對它沒有愛。
實做過幾次 ,我最後選擇回歸使用一般的 ASP.NET MVC Action 實做 Web API,不使用 MVC 提供的 ApiController 機制。身為 KISS (Keep It Simple, Stupid) 法則的忠實信徒,說穿只有一項考量 – 簡單!
貫徹 RESTful 精神是件麻煩事,導致在設計 API 介面時會受到諸多限制。舉一個最簡單的例子:我想要刪除四本書,BookId 分別為 9,5,2,7,若要求依循 RESTful 精神,做法就蠻分歧的:
- 跑迴圈 DELETE /books/9, DELETE /books/5, DELETE /books/2, DELETE /books/7 (絕對符合 RESTful,但有點蠢...)
- 先透過 POST /books/selections 將 9,5,2,7 四本書打包,賦與唯一資源代碼,例如 /books/pack32767,再 DELETE /books/pack32767 參考
- Amazon S3 REST 的做法是 POST /?delete,傳入包含要刪除項目識別碼的XML
- Facebook Graph API、Parse Server REST API、Google Drive REST API 則採用將多個 DELETE 作業打包成 JSON 放在一個 Request 裡送出,在伺服器收到後再解開一一執行。 參考
- 也有人主張 DELETE /books/9,5,2,7 之類的做法,但如此有沒有違背 REST 精神? 我不知道
除此之外,像是混合多個異種資源的更新 URI 該怎麼取?無明確資源對象的作業 URI 該用誰? 用 MVC Action 實做只要取個能望文生義的 Action 名稱,定義好傳入參數及傳回結果就可搞定,一旦被要求符合 RESTful,難度瞬間上升,發生次數多了,RESTful 帶來的優勢是否足以彌補額外增加的成本?到頭來有可能早已無關優劣利弊,流於「當然要 RESTful,不然別人會以為我們不懂」。
而另一方面,要配合 RESTful API,JavaScript 呼叫時也變得較複雜,需要自訂 HTTP Method,解析 HTTP Status Code,雖不是大事,但不能用最簡單的 $.get()、$.post() 搞定,測試偵錯變得麻煩些 。
上述提到種種問題,其實都能找到解,不然 RESTful API 如何能走到今天? 回到前面提到刪除多本書的例子,我總覺得原本單純可用 MVC Action DeleteBooks(string[] bookIds) 就能搞定的事,為了符合 RESTful 得大顯神通,有違 KISS 精神。
基於以上考量,設計 Web API 時我習慣寫成一般 ASP.NET MVC 方法而不用 ApiController,並一律限定 POST (多少降低一些 XSS 風險,參考:隱含殺機的GET式AJAX資料更新 ),執行結果無論成功失敗都傳回統一的 ApiResult 型別件:
/// <summary>
/// API呼叫時,傳回的統一物件
/// </summary>
public class ApiResult
{
/// <summary>
/// 執行成功與否
/// </summary>
public bool Succ { get; set; }
/// <summary>
/// 結果代碼(0000=成功,其餘為錯誤代號)
/// </summary>
public string Code { get; set; }
/// <summary>
/// 錯誤訊息
/// </summary>
public string Message { get; set; }
/// <summary>
/// 資料時間
/// </summary>
public DateTime DataTime { get; set; }
/// <summary>
/// 資料本體
/// </summary>
public object Data { get; set; }
public ApiResult()
{
}
/// <summary>
/// 建立成功結果
/// </summary>
/// <param name="data"></param>
public ApiResult(object data)
{
Code = "0000";
Succ = true;
DataTime = DateTime.Now;
Data = data;
}
/// <summary>
/// 建立失敗結果
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
public ApiResult(string code, string message)
{
Code = code;
Succ = false;
this.DataTime = DateTime.Now;
Data = null;
Message = message;
}
}
傳回型別統一,前則可撰寫共用 AJAX 呼叫函式,先統一處理錯誤再將資料拋回原呼叫端,錯誤代碼依系統別統一管理,在前後端產生對應。這樣的做法實際跑過幾過多個專案,沒有遇到什麼大問題,看來是可行的。(API 說明文件及測試程式我是自幹程式產生器搞定,必須承認不能套用現成工具額外多花了工夫,但量身訂做的西裝格外合身,整體上仍屬值得)
總體來看,我偏好用 ASP.NET MVC 寫 Web API 不走 RESTful ApiController,在這個議題上,應該會繼續非主流下去。