
掌握API建模:基本概念和實踐
"status": "success",
"data": {
"url": "https://www.wundergraph.com/blog/long_running_operations",
"sentiment": "positive"
}
}
但是,正如我們之前所說,此操作可能需要很長時間才能完成,并且用戶可能會取消它。
我們不應立即返回響應,而應返回具有唯一標識符的響應,以便客戶端可以輪詢服務器以獲取結果。
設計此類 API 的正確方法是返回 202 Accepted 狀態代碼。
因此,在這種情況下,我們的 API 響應可能如下所示。它將是一個帶有狀態代碼 202 和以下正文的響應。
{
"data": {
"url": "https://www.wundergraph.com/blog/long_running_operations",
"id": 1
},
"links": [
{
"rel": "status",
"href": "http://localhost:3000/api/v1/analyze_sentiment/1/status"
},
{
"rel": "cancel",
"href": "http://localhost:3000/api/v1/analyze_sentiment/1/cancel"
}
]
}
我們不是立即返回結果,而是返回一個鏈接列表,以便 API 的調用者可以獲取作業的當前狀態或取消它。
客戶端然后可以使用以下命令來獲取作業的狀態:
curl http://localhost:3000/api/v1/analyze_sentiment/1/status
太好了,我們現在已經將 API 設計為異步的。
接下來,我們將看看如何使用 GraphQL 來設計類似的 API。
與 REST 類似,我們可以使用 GraphQL 實現異步 API 來輪詢服務器的作業狀態。但是,GraphQL 不僅支持查詢和變更,還支持訂閱。這意味著,我們有更好的設計方法我們的 API 而不是強制客戶端輪詢服務器。
這是我們的 GraphQL 架構:
type?Job?{
??id:?ID!
??url:?String!
??status:?Status!
??sentiment:?Sentiment
}
enum?Status?{
????queued
????processing
????finished
????cancelled
????failed
}
enum?Sentiment?{
??positive
??negative
??neutral
}
type?Query?{
????jobStatus(id:?ID!):?Job
}
type?Mutation?{
??createJob(url:?String!):?Job
??cancelJob(id:?ID!):?Job
}
type?Subscription?{
??jobStatus(id:?ID!):?Job
}
創建作業將如下所示:
mutation?($url:?String!)?{
??createJob(url:?$url)?{
????id
????url
????status
??}
}
一旦我們得到了 id,我們就可以訂閱 Job 的變化:
subscription?($id:?ID!)?{
??jobStatus(id:?$id)?{
????id
????url
????status
????sentiment
??}
}
這是一個好的開始,但我們甚至可以進一步改進此架構。在當前狀態下,我們必須使“情感”可為空,因為該字段只有在作業完成后才有值。
讓我們的 API 更直觀:
interface?Job?{
??id:?ID!
??url:?String!
}
type?SuccessfulJob?implements?Job?{
??id:?ID!
??url:?String!
??sentiment:?Sentiment!
}
type?QueuedJob?implements?Job?{
??id:?ID!
??url:?String!
}
type?FailedJob?implements?Job?{
??id:?ID!
??url:?String!
??reason:?String!
}
type?CancelledJob?implements?Job?{
??id:?ID!
??url:?String!
??time:?Time!
}
enum?Sentiment?{
??positive
??negative
??neutral
}
type?Query?{
????jobStatus(id:?ID!):?Job
}
type?Mutation?{
??createJob(url:?String!):?Job
??cancelJob(id:?ID!):?Job
}
type?Subscription?{
??jobStatus(id:?ID!):?Job
}
將 Job 變成一個接口使我們的 API 更加明確。我們現在可以使用以下訂閱來訂閱作業狀態:
subscription?($id:?ID!)?{
??jobStatus(id:?$id)?{
????__typename
????...?on?SuccessfulJob?{
??????id
??????url
??????sentiment
????}
????...?on?QueuedJob?{
??????id
??????url
????}
????...?on?FailedJob?{
??????id
??????url
??????reason
????}
????...?on?CancelledJob?{
??????id
??????url
??????time
????}
??}
}
只有當 __typename 字段設置為“SuccessfulJob”時才會返回 sentiment 字段。
從上面的例子我們可以看出,REST 和 GraphQL 都可以用來設計異步 API。現在讓我們開始討論每種方法的優缺點。
首先,我要說的是,異步 API 的兩種方法都比同步 API 更好。無論您選擇使用 REST 還是 GraphQL,當一個操作需要超過幾秒鐘才能完成時,我總是建議您以異步方式設計 API。
現在,讓我們看看造成不同的微小細節。
REST 方法的一個巨大好處是一切都是資源,我們可以利用超媒體控件(Hypermedia)。讓我將這個“行話”翻譯成簡單的詞: REST API 的核心概念之一是每個“事物”都可以通過唯一的 URL 訪問。如果您向 API 提交作業,您將得到一個 URL,您可以使用該 URL 檢查作業的狀態。
相比之下,GraphQL 只有一個端點。如果你通過 GraphQL mutation 提交作業,你得到的是一個 ID! 類型的 id。如果你想檢查作業的狀態,你必須使用 id 作為Query 或 Subscription 類型的正確根字段的參數。作為開發人員,您如何知道 Job id 與 Query 或 Subscription 類型的根字段之間的關系?不幸的是,您不知道!
如果你想在設計你的 GraphQL schema 時做得很好,你可以將這些信息放入字段的描述中。但是,GraphQL 規范不允許我們像在 REST 中那樣使這些“鏈接”顯式。
相同的規則適用于作業的取消。使用 REST API,您可以向客戶端返回一個 URL,可以調用該 URL 來取消作業。
使用 GraphQL,您必須知道必須使用 cancelJob mutation 來取消作業并將 id 作為參數傳遞。
這聽起來可能有點夸張,因為我們使用的是一個非常小的 schema,但想象一下,如果我們的 Query 和 Mutation 類型上都有幾百個根字段。可能很難找到正確的根字段來使用。
缺乏資源和唯一 URL 似乎是 GraphQL 的弱點。但是,我們也可以反過來論證。
可以在 REST API 中返回帶有操作的鏈接。也就是說,我們從提交作業中返回的鏈接并不明顯。此外,我們也不知道例如是否應該使用 POST 或 GET 調用取消 URL。或者我們應該只刪除作業?
有額外的工具可以幫助解決這個問題。Kevin Swiber 的 Siren(https://github.com/kevinswiber/siren) 就是這樣的工具/規范。
如果你想設計好的 REST API,你絕對應該研究像 Siren 這樣的解決方案。
也就是說,考慮到 REST API 是占主導地位的 API 風格這一事實,Siren 已有 5 年多的歷史并且只有 1.2k 星表明存在問題。
對我來說,好的 (REST) API 設計似乎是可選的。大多數開發人員構建簡單的 CRUD 風格的 API,而不是利用超媒體的強大功能。
另一方面,由于缺乏資源,GraphQL 不允許您構建超媒體 API。但是,Schema 在 GraphQL 中是強制性的,迫使開發人員使他們的 CRUD 風格的 API 更加明確和類型安全。
在我個人看來,超媒體 API 比 CRUD 風格的 API 強大得多,但這種強大是有代價的,并且增加了復雜性。正是這種復雜性使 GraphQL 成為大多數開發人員更好的選擇。
作為 REST API 的開發者,你“可以”使用 Siren,但大多數開發者就是不在乎。作為 GraphQL API 的開發者,你“必須”有一個 Schema,沒有辦法繞過它。
如果你看看我們的 GraphQL 模式的第二個版本,接口的使用幫助我們使 API 非常明確和類型安全。它并不完美,我們仍然缺乏“鏈接”,但這是一個很好的權衡。
訂閱作業的狀態比輪詢狀態更優雅,這是顯而易見的。從 API 用戶的心智模型來看,訂閱事件流比輪詢狀態更直觀。
也就是說,沒有什么是免費的。
為了能夠使用訂閱,您通常必須使用 WebSockets。WebSockets 是有狀態的,它是有代價的。
將 WebSockets 添加到您的堆棧中也意味著 API 后端更加復雜。您的托管服務提供商是否支持 WebSockets?一些無服務器環境不允許長時間運行的操作或只是拒絕 HTTP 升級請求。WebSocket 連接的擴展方式也不同于短期連接HTTP 連接。
WebSockets 也是 HTTP 1.1 功能,這意味著您不能將它們與 HTTP/2 一起使用。如果您的網站對所有端點都使用 HTTP/2,則客戶端必須為 WebSocket 打開另一個 TCP 連接。
此外,WebSockets 可能無法在所有環境中工作,例如如果您使用反向代理或使用負載均衡器。
另一方面,HTTP 輪詢是一種非常簡單而乏味的解決方案。因此,雖然 GraphQL 訂閱為 API 用戶提供了一種更簡單的心智模型,但它們在實施方面帶來了巨大的成本。
請記住,您不會被迫將訂閱與 GraphQL 一起使用。您仍然可以通過使用查詢而不是訂閱來使用 HTTP 輪詢作業的狀態。
REST 和 GraphQL API 樣式都是設計同步和異步 API 的絕佳工具。它們各有優缺點。
GraphQL 對模式及其類型系統更加明確。另一方面,由于獨特的 URL 和超媒體控件,REST 可以更加強大。
我個人非常喜歡 Siren 的方法。但是,REST API 缺乏明確的架構給普通開發人員留下了太多的解釋空間。
借助正確的工具和良好的 API 治理,您應該能夠設計出色的 REST API。
有人可能會爭辯說 GraphQL 具有更多開箱即用的功能并且需要更少的治理,但我認為這不是真的。正如您從我們的 GraphQL 模式的兩個版本中看到的那樣,在設計方面有很大的靈活性GraphQL Schema。即使是 Schema 的第二個版本也可以進一步改進。
最后,我看不出一種解決方案比另一種解決方案好多少。在 API 設計上投入精力比在 REST 或 GraphQL 之間進行選擇要重要得多。
與您的用戶交談并弄清楚他們想如何使用您的 API。他們是習慣 REST API 還是 GraphQL API?他們會從 Subscriptions 而不是 WebSockets 中受益,還是他們更喜歡簡單無聊的輪詢?
也許你甚至不必在 REST 和 GraphQL 之間做出選擇。如果你可以構建一個很棒的 REST API,你可以輕松地用 GraphQL 包裝它,或者反過來。這樣,你可以為你的用戶提供兩種 API 樣式,如果這能給他們帶來價值。
你的收獲應該是良好的 API 設計和與你的用戶交流比選擇酷炫的技術重要得多。
文章轉自微信公眾號@云原生AI視界