傳說 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 最大的好處是風格統一,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 精神,做法就蠻分歧的:

  1. 跑迴圈 DELETE /books/9, DELETE /books/5, DELETE /books/2, DELETE /books/7 (絕對符合 RESTful,但有點蠢...)
  2. 先透過 POST /books/selections 將 9,5,2,7 四本書打包,賦與唯一資源代碼,例如 /books/pack32767,再 DELETE /books/pack32767 參考
  3. Amazon S3 REST 的做法是 POST /?delete,傳入包含要刪除項目識別碼的XML
  4. Facebook Graph API、Parse Server REST API、Google Drive REST API 則採用將多個 DELETE 作業打包成 JSON 放在一個 Request 裡送出,在伺服器收到後再解開一一執行。 參考
  5. 也有人主張 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() 搞定,測試偵錯變得麻煩些 。

至於使用 HTTP Status Code 302/401/404/500 傳遞狀態,以 .NET WebClient.UploadData() 呼叫時將被視為 Exception,需要 try ... catch 攔截,會增加些許困援。

[2023-12-09 更新] DevOps 時代,出錯時建議回傳 HTTP 500 以提高可觀測性。

上述提到種種問題,其實都能有解,不然 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,在這個議題上,應該會繼續非主流下去。

My survey of "does WebAPI must follow RESTful?".


Comments

# by Sam Lin

Generally, I agree. However, I actually mix them together. I only use get and post. Also I embrace http status code with custom result just like you did. You can use ActionResult<T> as return type and StatusCode(code, object). I think this way, it somehow get the benefit from both sides.

# by Jeffrey

to Sam Lin, 在客戶端是 .NET 的場合,我有時會額外提供封裝 Web API 呼叫的 DLL 元件給客戶端,此時就會用 ApiResult<T>,在客戶端享受強型別的好處。如果用到 HTTP Status Code 401、404、500,若用單純的 WebClient.UploadData() 呼叫得用 try ... catch 捕捉這些狀態碼,有點小麻煩。

# by WeiHan

喜歡使用ASP.NET MVC Action 實做 Web API 實際專案也是以此居多 > JavaScript 呼叫時也變得較複雜.. 這點接觸過寫前端的人不想管那麼多商業邏輯 只想簡單一個呼叫API就把事情都做到好,返回結果呈現在前端

# by Holmes

我覺得兩種都有應用的情境 , 例如一些 API 是適用於所有的 Application , 例如 Address , Paymethod , Payment etc... 但是有些只是適用於特定的情境或 Application , 使用 Mvc Action 就很適合

# by SUEZOU

保持一致就好了吧,給自己用的就是要在地化呀。

# by Mark

不好意思,剛在研究 Web API 就看到您的文章,我也想用 ASP.NET MVC Action 來開發 Web API,想請教您的是 ASP.NET MVC Action 是指什麼?使用 Visual Studio 開發需要選擇什麼專案呢?不知道能否提供一個簡單的範例以供參考。不好意思,問題有點多,希望可以得到您的回覆,感恩。

# by Jeffrey

to Mark, 單用文字描述有點難說清楚,這幾天我整理一篇簡單介紹好了。

# by Mark

謝謝

# by Alex

可是 沒有規定 用 ApiController 就一定要 RESTful 啊

# by kuo

感謝黑大分享,最近剛開始接觸restful設計,你的文章讓我雜亂的頭緒稍微有了心得

# by EricHsu

想請教 "可使用 QueryString,但只應拿來傳遞額外過濾條件或參數,不應包含識別資源的鍵值" 不應包含識別資源的鍵值,這一點的考量為何?

# by Jeffrey

to EricHsu,個人觀點,應沒什麼特別道理,純粹是REST的設計哲學。定出規範或慣例可維持所有API的一致性,看到 URL 時很容易理解。而其中一個規則是「定位資源用的識別碼一律放在URL,Query Parameter用來放額外條件過濾或參數」 參考:https://blog.toright.com/posts/5523/restful-api-%E8%A8%AD%E8%A8%88%E6%BA%96%E5%89%87%E8%88%87%E5%AF%A6%E5%8B%99%E7%B6%93%E9%A9%97.html

# by

其餘也罷,寫code變複雜這點不能同意 不管前後端都是一個service可以做好的東西, 如果是用angular 7 8本來httpClient就提供 GET、POST、PUT、DELETE 即使沒有,做一個service任何專案也能一直沿用

Post a comment