由 GraphQL 來思考如何做一個好的 API Design

shanyue發表於2019-03-15

目前我已經寫了一年多 graphql,也時常思考和 Rest API 的不同,以及對 API Design 的啟發。

他山之石可以攻玉。qraphql 一些天然的設計或者思想對寫 Rest API 有很大的借鑑或參考意義。

這裡總結下一些受啟發的 API 設計規範。

如果你對 graphql 不熟悉,可以先參考 graphql 中文文件

本文連結 shanyue.tech/post/api-de…

對所有的資源返回 id

在 graphql 中,scalar 型別 ID 用來表示資源的全域性唯一性。在 apollo-client 中也建議客戶端每次請求都把 id 帶上。

在響應中帶上 id 至少有兩個好處

  1. 客戶端對資源的快取
  2. 在資料上游至客戶端的整個鏈路中有利於資料的溯源

按需載入資源的欄位

query TODO {
  todo (id: 10) {
    id 
    name
    status
  }
}
複製程式碼

如客戶端只需要顯示某個 TODO 的狀態以及名稱,則只需要返回 name 以及 status 欄位,大大減少了網路的流量。

另外, graphql server 需要在資料庫層面也對欄位做按需載入。否則,graphql server 與 database 之間也會造成無用的資料 IO 與流量浪費。

獲取 graphql query 所請求的欄位,需要手動解析 GraphQLFieldResolveFn 函式的第四個欄位 info,並在每一個 field 上自定義一個 directive 標註 Graphql Filed 與 Database Field 的關係

在 Rest API 中可以使用額外欄位做按需載入。 如使用 fields 標記返回需要的欄位,若無此欄位,預設返回資源的全部欄位,在中介軟體中對 fields 做結構化處理

// 請求 Todo:10,並且只需要 id,name,status 三個字元安
'/api/todos/10?fields=id,name,status'

// 請求 Todo:10 全部資源
'/api/todos/10'
複製程式碼

關聯資源使用巢狀物件表示

這個請求表示一個使用者列表,每個使用者需要展示最後一個 Todo 的名稱。Todo 需要使用巢狀物件來表示。

query USERS {
  users {
    id
    name
    lastTodo {
      id
      name
    }
  }
}
複製程式碼

在 Rest API 設計中經常見到所有資料進行了展開,不僅無法定位資源,也不好擴充套件資料。巢狀資料可以很靈活的擴充套件資料,另外也可以對巢狀資料進行按需載入

const res0 = {
  users: [{
    id: 1,
    name: "山月",
    todoName: "學習"
  }]
}

// 修改後
const todoFields = {}
const res = {
  users: [{
    id: 1,
    name: "山月",
    todo: {
      id: 1,
      name: "學習",
      ...fields
    }
  }]
}

// 可以這樣設計 API
const api = '/api/users?fields=id,name,todo.id,todo.name'
複製程式碼

使用 ISOString 表示時間戳

在 graphql 中,雖沒有一個 scalar 型別來表示時間戳,不過可以自定義 scalar DateTime 來表示時間。關於時間的格式

參考 StackOverflow 上的問題 the-right-json-date-format

const date = new Date()

// 從 toJSON 的輸出就知道前後端互動需要使用什麼格式了
date.toJSON()
// 2019-03-14T07:41:08.500Z
date.toISOString()
// 2019-03-14T07:41:08.500Z
複製程式碼

這樣返回的格式不僅符合規範,而且可讀性也比較好。

我見過API中返回的時間戳表示為 unix timestamp,js timestamp, iso8601 三種格式,較為混亂。統一的資料格式有利於前後端的聯調,不過這也得益於 graphql 的強型別 schema。

結構化的錯誤資訊

在 graphql 中會返回 { data, errors } 的資料結構,可以在最後結構化錯誤資訊為

{
  "code": "InvalidToken",
  "message": "Token 失效",
  "httpStatus": 401
}
複製程式碼

message 為可讀性的錯誤資訊,可以由前端直接顯示,code 為除錯用,httpStatus 由下一步的中介軟體捕捉,設定狀態碼。

在結構化錯誤資訊後,可以順帶把錯誤資訊傳送到報警系統 (如 Sentry)。不過需要分清 WARN 與 ERROR,如 401,403 應當做 WARN 處理。

符合標準的 http status

恩,好吧。graphql 這條有缺陷。graphql 的 QueryMutation 都是使用 POST 請求。對不同的執行成功的 Mutation 返回不同的 200,201,202 還是比較麻煩。

不過對於錯誤返回不同的狀態碼, 開啟 devtool 一眼可以看到紅色的 4XX 資訊,也對快速定位錯誤請求有幫助,稍微減少了些煩躁心。

介紹幾種常見的4xx狀態碼

  • 401 Unauthorized: 使用者未登入請求需要登入才能請求的資源
  • 403 Forbidden: 使用者A登入了,但他卻想請求 B 的資源
  • 400 Bad Request: 恩,我把所有找不到狀態碼的錯誤都放到了 400

關於400參考 400 BAD request HTTP error code meaning? 這裡有一篇文章,關於4xx狀態碼的選擇,取一張圖出來

如何選擇http錯誤狀態碼

請求及響應資料校驗

由於 graphql 的強型別 schema,也省了資料輸入輸出的校驗。

對於 Rest API,可以使用 JSON Schema 來校驗資料格式。node 也可以使用 joi 做資料校驗。

這裡放一份 JSON Schema 的文件:json-schema.org/

註釋文件化

得益於 graphql 的 introspection 與強型別的 schema。graphql 可以根據原始碼以及註釋自動生成文件,直接使用 graphiql 或者 graphql playground 上檢視。

如果你使用 node.js 來寫伺服器應用,可以使用 apiDoc

另外,注意不要把文件暴露到生產環境,graphql 需要在生產環境中關掉 introspection。

相關文章