標籤:

再談 API 的撰寫 - 總覽

背景

去年我寫過一篇文章:撰寫合格的 REST API。當時 Juniper 裁掉了我們在德州的一支十多人的團隊,那支團隊有一半的人手在之前的半年裡,主要的工作就是做一套 REST API。我接手這個工作時發現那些API寫的比較業餘,沒有考慮幾個基礎的HTTP/1.1 RFC(2616,7232,5988等等)的實現,於是我花了些時間重寫,然後寫下了那篇文章。

站在今天的角度看,那時我做的系統也有不少問題,很多 API 之外的問題沒有考慮:

  • API 的使用文檔。當時我的做法是把文檔寫在公司使用的協作系統 confluence 里,但這樣做的最大的問題是:代碼和文檔分離,不好維護。

  • API 的監控。整個 API 系統沒有一個成體系的監控機制,各種 metrics 的收集嚴重依賴 API 的實現者處理,拿時髦的話說就是沒法 orchestrate。

  • API 的測試。做過大量 API 工作的人都知道,為 API 寫測試用例是非常痛苦的事情,你不但要對 API 使用的代碼做 unit test,還需要對 API 本身做 smoke test(最基本的 functional test),保證所有 API 是可用的,符合預期的。由於需要撰寫的測試用例的數量巨大,一般我們寫寫 unit test 就了事。

理想情況下,一個 API 撰寫完成,應該能夠自動生成文檔和測試用例,而 API 系統也應該提供一整套統計的 API 用於生成 metrics。預設情況下,API 系統本身就應該收集很多 metrics,比如每個 API 的 response time,status code 等等,使用 collectd / statd 收集信息,並可以進一步發送到 datadog / new relic 這樣的 APM 系統。

在 adRise,我們有一套運行了數年的 API 系統,不符合 RFC,(幾乎)沒有文檔,(幾乎)沒有測試,(幾乎)沒有監控,最要命的是,它的開發效率和運行效率都不高。因此,過去的一兩個月,我主導開發了一個全新的 API 系統。

目標

在打造一個新的系統之前,我們需要確立一些目標。這是我在設計 API 時寫下的一些目標:

  • A well defined pipeline to process requests

  • REST API done right (methods, status code and headers)

  • Validation made easy

  • Security beared in mind

  • Policy based request throttling

  • Easy to add new APIs

  • Easy to document and test

  • Introspection

其中,introspection 包含兩層意思:

  • API 系統自動收集 metrics,自我監控

  • 無論是撰寫者,還是調用者,都很很方便的獲取想要獲取的信息

選型

有了以上目標,接下來的就是進行技術選型。技術選型是無法脫離團隊單獨完成的,如果讓我個人選擇一個基礎語言和框架,我大概會選擇基於 Erlang/OTP,使用 Elixir 開發的 Phoenix,或者,乾脆使用 Plug(Phoenix 的基石)。因為 Plug / Phoenix 通過組合來構建 pipeline 的方式很符合我的思維,Elixir 對 macro 的支持和 Erlang 語言核心的 pattern matching 讓諸如路由這樣的子系統高效簡潔美觀,而 Erlang / OTP 的高並發下的健壯性又是一個 API 系統苦苦追求的。

然而,我需要考慮團隊的現實。在 adRise,我們使用 node.js 作為後端的主要技術棧(還有一些 PHP / Python / scala),因此 API 系統最好是基於 node.js 來完成。node.js 下有很多適合於寫 API 的框架,比如說:express,restify,hapi,loopback,sails.js 等。在綜合考察了這些框架之後,我選擇了 restify,原因有三:

  • 介面和結構非常類似 express(團隊對此非常有經驗),但比 express 更專註於 REST API

  • 一系列 middleware 和 route actions 可以組成一個靈活高效的 pipeline

  • 簡單,可擴展性強,容易和其他庫結合,很適合作為一個新的框架的起點

  • 源代碼很好理解,一天內就能讀完(好吧這是個湊數的原因)

事實證明,這是個還算不錯的選擇。

定下了基礎框架,接下來就是選擇核心的組件。首先就是 validator。很多人做系統並不重視 validator,或者沒有一個統一的視角去看待 validator,這樣不好。任何一個系統的運行環境都是個骯髒的世界,到處是魑魅魍魎,污穢不堪;而我們希望系統本身是純凈的,是極樂凈土,那怎麼辦?

簡單,凈化輸入輸出。對於一個 API,什麼樣的 header,body 和 querystring 是被允許的?什麼樣的 response body 是合格的?這個需要定義清楚。所以我們需要一個合適的 validator。如果說挑框架似四郎選秀女,環肥燕瘦讓你眼花繚亂,選 validator 就像姜維點將,看來看去只有王平廖化堪堪可用。在 github 里逛了半天,最後能落入法眼的也只有 joi 和 json schema 可用。

json schema 其實很好用,很貼近各類 API 工具的 schema(swagger 直接就是用 json schema),可惜太 verbose,讓程序員寫這個有點太啰嗦:

{n "title": "Example Schema",n "type": "object",n "properties": {n "firstName": {n "type": "string"n },n "lastName": {n "type": "string"n },n "age": {n "description": "Age in years",n "type": "integer",n "minimum": 0n }n },n "required": ["firstName", "lastName"]n}n

而 joi 是 hapi 提供的 validator,介面很人性化,相同的 schema,描述起來代碼量只有前者的 1/3:

joi.object().keys({n firstName: joi.string().required(),n lastName: joi.string().required(),n age: joi.number().min(0).description(Age in years),n});n

而且它還可以比較容易地逆向輸出(當然,需要各種適配)成 json schema。輸出成 json schema 有什麼好處?可以用來生成 swagger doc!swagger 是一種 API 描述語言,可以定義客戶端和伺服器之間的協議。swagger doc 可以生成 API 的文檔和測試UI,比如說:

在接下來的文章中,我會詳細介紹 swagger。

我們再看 ORM。經常使用 express 的同學應該了解,express 本身並不對你如何存取數據有過多干涉,任何人都可以按照自己的需求使用其所需要的數據訪問方式:可以是 raw db access,也可以使用 ORM。這種靈活性在團隊協作的時候是種傷害,它讓大家很容易寫出來風格很不統一的代碼,而且,在寫入資料庫和從資料庫中讀取數據的 normalization,離了 ORM 也會帶來很多 ad-hoc 的代碼。因此,儘管 ORM 背負著很多罵名,我還是希望在涉及數據訪問的層面,使用 ORM。

我們的系統的資料庫是異構的,因此,純種的,只對一類資料庫有效的 ORM,如 Mongoose / Sequelize 就不太合適,上上之選是介面支持多種不同資料庫,在需要特殊查詢或者操作的時候還能轉 native 的 ORM。這樣,讓工程師的效率和系統的效率達到一個平衡。在 node.js 下,這樣的 ORM 不多,可用的似乎只有 waterline。waterline 是 sails.js 開源的一個 ORM,支持多種 db 的混合使用,在各個資料庫無法統一的操作介面上(比如 mongodb 的 upsert),你可以方便地將其生成的 model 轉 native,直接使用資料庫的介面。

此外,waterline 的 model 的 schema 使用 json 來描述,這使得它可以很方便地轉化成 joi schema,在系統的進出口進行 validation。

// waterline schemanconst attributes = {n user_id: { type: integer, required: true },n content_id: { type: integer, required: true },n content_type: { type: string, required: true },n}n

接下來是日誌系統。一套 API 系統可能包含多台伺服器,所以日誌需要集中收集,處理和可視化。一般而言,我們可以用 ELK,或者第三方的服務。如果在設計系統之初就考慮日誌的集中管理,那麼日誌的收集應該考慮用結構化的結構,而非字元串。字元串儘管可以使用 grok 來處理,但畢竟效率低,還得為每種日誌寫 grok 的表達式。由於 node restify 預設使用 bunyan 作日誌,而 bunyan 可以生成 json 格式的日誌,因此直接滿足我們的需求。

最後我們再看 test framework。一個合格的系統離不開一套合適的 test framework。我的選擇是 ava / rewire / supertest / nyc。ava 是一個 unit test framwork,和 mocha / tape 等常見的 test framework 類似,解決相同的問題,不過 ava 能夠並發執行,效率很高,而且對 es6 支持很棒,test case 可以返回 Promise,ava 處理剩下的事情。有時候我們需要測試一個模塊里沒有 export 出來的函數,或者 Mock 一些測試時我們並不關心的函數,rewire 可以很方便地處理這樣的問題。supertest 可以做 API 級別的測試,也就是 functional testing,而 nyc 可以用來做 test coverage。

今天先講這麼多,下次談談如何架構一個 API 系統。


推薦閱讀:

談談編譯和運行
API 是如何工作的(傻瓜式教學)
做 2D 俄羅斯方塊用什麼繪圖 API 比較好?
了解Revit API

TAG:API |