編寫 Node.js Rest API 的 10 個最佳實踐

王仕軍發表於2017-03-03

Node.js 除了用來編寫 WEB 應用之外,還可以用來編寫 API 服務,我們在本文中會介紹編寫 Node.js REST API 的最佳實踐,包括如何命名路由、進行認證和測試等話題,內容摘要如下:

  1. 正確使用 HTTP Method 和路由
  2. 正確的使用 HTTP 狀態碼
  3. 使用 HTTP Header 來傳送後設資料
  4. 為 REST API 挑選合適的框架
  5. 要對 API 進行黑盒測試
  6. 使用基於 JWT 的無狀態的認證機制
  7. 學會使用條件請求機制
  8. 擁抱介面呼叫頻率限制(Rate-Limiting)
  9. 編寫良好的 API 文件
  10. 對 API 技術演化保持關注

1. 正確使用 HTTP Method 和路由

試想你正要構建一個 API 用來建立、更新、獲取、刪除使用者,對於這些操作,HTTP 規範裡面已經有了現成的操作:POST、PUT、GET、DELETE,建議直接使用他們來描述介面的行為。

至於路由的命名,應該使用名詞或名詞性短語來作為資源識別符號,比如上文提到的使用者管理的例子,路由就應該長這樣:

  • POST /users 或者 PUT /users/:id 用來建立新使用者;
  • GET /users 用來獲取使用者列表;
  • GET /users/:id 用來獲取單個使用者;
  • PATCH /users/:id 用來更新使用者資訊;
  • DELETE /users/:id 用來刪除使用者;

2. 正確的使用 HTTP 狀態碼

如果伺服器端在請求處理的過程中出錯了,你必須設定正確的響應狀態碼,具體如下:

  • 2xx,表示一切正常;
  • 3xx,表示資源位置已經更改;
  • 4xx,表示因為客戶端錯誤而導致請求無法被處理,比如引數校驗沒通過;
  • 5xx,表示因為伺服器錯誤導致請求無法被處理,比如服務端拋了異常;

如果你使用 express,設定狀態碼非常簡單:res.status(500).send({ error: ‘Internal server error happend’ }),如果使用了 restify,也是類似的:res.status(201)。

如果想看完整的 HTTP 狀態碼,點選這裡

3. 使用 HTTP Header 來傳送後設資料

如果想要傳送關於響應體資料的後設資料,可以使用 Header ,Header 可以包含的常見後設資料包括如下幾類:

  • 分頁資訊;
  • 頻率限制資訊;
  • 認證資訊;

如果你需要在 Header 中傳送自定義的後設資料,最好的做法是在 Header 名稱前面加 X,例如,需要傳送 CSRF Token 的時候,實際的 Header 應該命名為:X-CSRF-Token,然而,這種 Header 在 RFC 6648 中已經被廢棄了。API 在設定自定義 Header 的時候還要儘可能避免命名衝突,比如為了達到這個目的OpenStack 為所有 API 的自定義 Header 都加上了 OpenStack 的字首:

OpenStack-Identity-Account-ID  
OpenStack-Networking-Host-Name  
OpenStack-Object-Storage-Policy

需要注意的是,雖然 HTTP 規範中沒有規定 Header 的大小,但是 Node.js 中 Header 的大小被限制在了 80KB。官方原文如下:

不要讓 HTTP Header ,包括其中狀態碼那行的整體大小超過 HTTP_MAX_Header_SIZE,這樣做的目的是為了防禦基於 Header 的 DDOS 攻擊。點選這裡

4. 為 REST API 挑選合適的框架

根據你的實際場景挑選合適的框架是非常重要的,Node.js 中的框架大致介紹如下:

Express、Koa、HAPI

Express、Koa、HAPI 主要是用來構建瀏覽器 WEB 應用,因為他們都支援服務端模板渲染,雖然這只是他們眾多功能中的一個。如果你的應用需要提供使用者介面,那麼這三個就是不錯的選擇。

Restify

而 Restify 是專門用來建立符合 REST 規範的服務的,他誕生的目的就是幫你構建嚴格意義上的、可維護的 API 服務。Restify 內建了所有請求處理函式的 DTrace 支援。並且已經被 npm 和 netflix 用來在生產環境提供重要的服務。

5. 要對 API 進行黑盒測試

測試 API 的最好辦法是對他們進行黑盒測試,黑盒測試是一種不關心應用內部結構和工作原理的測試方法,測試時系統任何部分都不應該被 mock。

supertest 是可以用來對介面進行黑盒測試的模組之一,下面是基於測試框架 mocha 編寫的一個測試用例,該用例的目的是檢查介面是否能返回單條的使用者資料:

const request = require('supertest')

describe('GET /user/:id', function() {
  it('returns a user', function() {
    // newer mocha versions accepts promises as well
    return request(app)
      .get('/user')
      .set('Accept', 'application/json')
      .expect(200, {
        id: '1',
        name: 'John Math'
      }, done);
  });
});

可能有人會問:API 服務所連線的資料庫裡面的資料是如何寫進去的呢?

通常來說,你寫測試的時候,要儘可能不對系統狀態做假設,然而在某些場景下,你需要準確的知道系統當前所處的狀態以增加更多的斷言來提高測試覆蓋率。如果你有這種需求,你可以試用如下的方法對資料庫進行預填充:

  • 選擇生產環境資料的子集來執行黑盒測試;
  • 執行黑盒測試之前把手工構造的資料填充到資料庫中。

此外,有了黑盒測試並不意味著不需要單元測試,針對 API 的單元測試還是需要編寫的。

6. 使用基於 JWT 的無狀態的認證機制

因為 Rest API 必須是無狀態的,因此認證機制也需要是無狀態的,而基於 JWT(JSON Web Token) 的認證機制是無狀態認證機制中的最佳解決方案。

JWT 的認證機制包含三部分:

  1. Header:包含 token 的型別和雜湊演算法;
  2. payload:包含宣告資訊;
  3. signature:JWT 實際上並不是對 payload 進行加密,只是對其做了簽名;

為 API 新增基於 JWT 的認證機制也非常的簡單,比如下面的程式碼:

const koa = require('koa');
const jwt = require('koa-jwt');

const app = koa();

app.use(jwt(
  secret: 'very-secret'
}));

// Protected middleware
app.use(function*() 
  // content of the token will be available on this.state.user
  this.body = { secret: '42' }
});

有了如上的程式碼,你的 API 就有了 JWT 的保護。如果要訪問這種被保護的介面,需要使用 Authorization Header 來提供 token,比如:

curl --Header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" my-website.com

你可能注意到了,JWT 模組並不依賴任何資料儲存層,這是因為 token 本身是可以單獨被校驗的,token 裡面的 payload 甚至可以包含 token 的簽名時間、有效期限。

此外,你還需要確保,所有的 API 介面只能通過更安全的 HTTPS 連結來訪問。

7. 學會使用條件請求機制

條件請求機制是基於不同的 Header 表現出不同的行為的機制,可以認為這些 Header 就是請求處理方式的先決條件,如果條件滿足,請求處理方式就會有所不同。

可以利用這些 Header 檢測伺服器上的資源版本是否匹配特定的資源版本,這些 Header 的取值可以是如下的內容:

  • 資源的最後修改時間;
  • 資源的標籤(隨資源變化而變化);

具體來說:

  • Last-Modified:標識資源的最新修改時間;
  • Etag:標識資源的標籤;
  • If-Modified-Since:結合 Last-Modified Header 使用;
  • If-Non-Match:結合 Etag 使用;

下面來看一個實際的例子:

客戶端不知道 doc 資源的任何版本,所以請求時即不能提供 If-Modified-Since,也不能提供 If-Non-Match 兩個 Header,然後服務端在響應中會增加 Etag 和 Last-Modified 兩個 Header。

接下來,客戶端再次請求相同的資源的時候,就可以帶上 If-Modified-Since 和 If-Non-Match 這兩個 Header 了,然後如果伺服器端會檢查資源是否修改,如果沒有修改,直接返回 304 – Not Modified 狀態碼,而不重複傳送資源的內容。

8. 擁抱介面呼叫頻率限制(Rate-Limiting)

頻率限制是用來控制呼叫方有對介面發起請求的次數,為了讓你的 API 使用者知道他們還剩下多少餘額,可以設定下面的 Header:

  • X-Rate-Limit-Limit:特定時間段內允許的最多請求次數;
  • X-Rate-Limit-Remaining:特定時間段內剩餘的請求次數;
  • X-Rate-Limit-Reset:什麼時候請求頻率限制次數會重置;

大多數的 WEB 框架都支援上面這些 Header,如果內建不支援,也可以找到外掛來支援,比如,如果你使用了 koa,可以使用 koa-rate-limit

需要注意的是,不同的 API 服務提供商頻率限制的時間窗差異會很大,比如 GitHub 是 60 分鐘,而 Twitter 是 15 分鐘。

9. 編寫良好的 API 文件

編寫 API 的目的當然是讓別人使用並受益,提供良好的介面文件至關重要。下面這兩個開源專案可以幫你建立 API 文件:

如果你願意使用第三方文件服務商,可以考慮 Apiary

10. 對 API 技術演化保持關注

過去幾年中,API 技術方案領域出現了兩種新的查詢語言,分別是 Facebook 的 GraphQL 和 Netflix 的 Falcor,為什麼需要他們呢?

試想這種 API 介面請求:/org/1/space/2/docs/1/collaborators?include=email&page=1&limit=10,類似的情況會讓 API 很快失控,如果你希望所有介面能返回類似的響應格式,那麼 GraphQL 和 Falcor 就能幫你解決這個問題。

關於 GraphQL

GraphQL 是一種用於 API 的查詢語言,也是一種基於現有資料處理資料查詢的執行時。GraphQL 為您的 API 中的資料提供了一個完整和可理解的描述,使使用者能夠準確地詢問他們需要什麼,使得隨著時間推移的 API 演化更容易,GraphQL 還有強大的開發工具支援。 到這裡閱讀更多。

關於 Falcor

Falcor 是支撐著 Netflix UI 的創新資料平臺。Falcor 允許你將所有後端資料建模為 Node.js 服務商的單個虛擬 JSON 物件。在客戶端可以使用熟悉的 JavaScript 操作、處理遠端JSON物件。如果你知道你的資料,你就知道你的 API 長啥樣。 到這裡閱讀更多。

能帶來靈感的優秀 API 設計

如果你正在開發 Rest API 或者準備改進老版本的 API,這裡收集了幾個線上上提供服務、設計優秀並且非常直接借鑑的 API:

希望讀到這裡的同學對如何用 Node.js 編寫良好的 API 有更好的理解,如果有建議,歡迎評論中提出。

相關文章