在本文中,我們將以一個簡單的博客應用為例進行說明。假設我們有兩個模型:用戶和帖子,它們之間存在一對多的關聯關系,即一個用戶可以發布多個帖子。

針對這個示例,我們設定了以下終端節點:

1. /users
2. /users/<id>
3. /users/<id>/posts
4. /posts
5. /blog/posts/<id>
6. /blog/posts/<id>/user

如果您想使用這個 API,可以查看此存儲庫中的代碼。

通過此設計,我們可以實現以下查詢功能:

  1. 獲取用戶列表
  2. 根據用戶ID查詢用戶信息
  3. 獲取帖子列表
  4. 根據帖子ID查詢帖子詳情
  5. 根據帖子ID查詢其作者信息

實際上,在REST API的設計中,返回的數據結構主要有兩種表現形式:平面式和嵌套式。

REST API 的平面化設計:

在平面設計中,模型間的關聯關系通常通過相關項目的ID來體現。

例如,對/users端點的調用將返回許多用戶對象,每個用戶對象都有一個字段postIds。此字段包含每個用戶關聯的帖子的ID數組:

[
{
"id": "user-0",
"name": "Nikolas",
"postIds": []
},
{
"id": "user-1",
"name": "Sarah",
"postIds": ["post-0", "post-1"]
},
{
"id": "user-2",
"name": "Johnny",
"postIds": ["post-2"]
},
{
"id": "user-3",
"name": "Jenny",
"postIds": ["post-3", "post-4"]
}
]

REST API 的嵌套布局設計:

盡管平面化的API設計因其簡潔明了而備受青睞,但它也可能導致客戶端面臨N+1請求問題:想象一下這樣的場景:客戶端(如Web應用程序)需要展示一個頁面,該頁面需包含用戶列表及他們最新的文章標題。

若采用平面化設計,你首先需向/users端點發送請求,獲取用戶列表及與他們相關的帖子ID。隨后,你需為每個帖子ID單獨向/blog/posts/<id>端點發送請求,以獲取相應的標題。這種做法不僅效率低下,還可能導致性能瓶頸。

為解決這一問題,你可以考慮采用嵌套的API設計。在這種設計中,每個user對象都會直接包含一個posts數組,該數組內嵌入了完整的post對象,而非僅僅是ID列表。這樣一來,客戶端便可通過一次請求即可獲取所有所需信息,從而有效避免N+1請求問題。

[
{
"id": "user-0",
"name": "Nikolas",
"posts": []
},
{
"id": "user-1",
"name": "Sarah",
"posts": [
{
"id": "post-0",
"title": "I like GraphQL",
"content": "I really do!",
"published": false
},
{
"id": "post-1",
"title": "GraphQL is better than REST",
"content": "It really is!",
"published": false
}
]
},
{
"id": "user-2",
"name": "Johnny",
"posts": [
{
"id": "post-2",
"title": "GraphQL is awesome!",
"content": "You bet!",
"published": false
}
]
},
{
"id": "user-3",
"name": "Jenny",
"posts": [
{
"id": "post-3",
"title": "Is REST really that bad?",
"content": "Not if you wrap it with GraphQL!",
"published": false
},
{
"id": "post-4",
"title": "I like turtles!",
"content": "...",
"published": false
}
]
}
]

確實,嵌套方法雖然解決了N+1請求問題,但它也帶來了自身的挑戰。對于包含大型模型對象的API,帶寬使用(特別是在移動設備上)可能會成為制約因素。另一個問題是過度獲取,即客戶端可能下載了大量并不需要的數據,從而浪費了用戶的帶寬資源。

此外,即使采用嵌套方法,也不能保證總是能獲得所需的所有數據。例如,如果帖子還與評論相關聯,并且界面需要顯示每篇文章的最后三條評論,那么這種嵌套關系可能會變得非常復雜且難以管理。隨著嵌套層級的增加,每個層級都會帶來額外的復雜性和性能開銷。

REST API 的混合布局設計

在實際開發過程中,我們往往會發現終端節點返回的數據是根據具體應用場景來定制的。這意味著前端團隊會詳細列出其數據需求,并與后端團隊進行溝通,以確保終端節點返回的有效負載中包含所有必要的信息。

然而,這種做法在軟件開發流程中帶來了額外的復雜性。每當前端界面涉及數據展示的設計發生變更時,都可能需要后端團隊的直接介入。這不僅會延長開發周期,還可能阻礙快速的用戶反饋和迭代進程。

此外,這種方法還存在其他弊端。首先,它非常耗時且容易出錯,因為每次變更都需要前端和后端團隊的緊密協作。其次,頻繁變動的API很難維護,這不僅增加了后端團隊的工作量,還可能導致客戶端因數據不一致而出現運行錯誤。特別是當某些API響應中的字段被刪除而客戶端未及時更新時(或者客戶端仍在使用較舊的API版本),很可能會因為缺少必要的數據而崩潰。

因此,在采用混合布局設計REST API時,我們需要謹慎權衡各種因素。既要確保前端團隊能夠獲得所需的數據,又要避免給后端團隊帶來過大的維護負擔,同時還要確保API的穩定性和可靠性。通過綜合考慮這些因素,我們可以設計出既滿足業務需求又具有良好擴展性和可維護性的REST API。

GraphQL:為客戶提供靈活性與安全性的優選方案

GraphQL 能夠有效地解決我們之前提到的N+1問題、過度獲取數據以及迭代周期緩慢等挑戰。

GraphQL與REST之間的核心差異可以概括為以下兩點:

GraphQL之所以能夠實現這一點,關鍵在于客戶端在請求數據時擁有更大的主動權。客戶端可以向服務器提交一個查詢,明確描述其所需的數據結構。服務器在接收到查詢后,會進行解析,并僅返回客戶端明確請求的數據。

在GraphQL中,客戶端通過發送查詢來指定期望的響應結構,而這些查詢將由服務器進行解析。

GraphQL的數據查詢方式在其架構定義中得到了明確的規定。因此,客戶端無法請求不存在的字段,從而確保了數據的安全性。此外,GraphQL支持查詢嵌套,這意味著客戶端可以在單個請求中同時獲取相關聯項目的信息,從而有效避免了N+1問題的發生。這無疑是一個令人稱贊的特性!

通過三個簡單步驟用GraphQL包裝REST API

在本節中,我們將概述如何通過 3 個簡單的步驟使用 GraphQL 包裝 REST API。

概述:GraphQL 服務器的工作原理

GraphQL其實并不復雜!它遵循一些基礎而簡單的原則,這使得它極具靈活性和廣泛適用性。

構建GraphQL API通常包含兩個核心步驟:首先,您需要定義一個GraphQL架構;其次,您需要為這個架構實現解析器函數。

這種方法的好處在于其高度的迭代性,這意味著您無需提前為API規劃完整的架構。相反,您可以根據實際需求逐步添加類型和字段(這與逐步實現REST API端點的方式類似)。這就是“Schema-driven”或“Schema-First development”的核心理念。

GraphQL解析器函數的數據來源十分廣泛,可以是從SQL或NoSQL數據庫、REST API、第三方API、遺留系統,甚至是其他GraphQL API中獲取。

GraphQL之所以如此靈活,很大程度上是因為它并不依賴于特定的數據源。解析器函數幾乎可以從任何來源獲取數據。

這種特性使得GraphQL成為包裝REST API的理想工具。當使用GraphQL包裝REST API時,您需要完成以下三個步驟:

  1. 分析REST API的數據模型:首先,您需要深入了解您要包裝的REST API的數據結構。
  2. 根據數據模型構建GraphQL架構:接下來,基于REST API的數據模型,您需要設計一個相應的GraphQL架構。
  3. 為GraphQL架構實現解析器函數:最后,您需要為GraphQL架構中的每個類型和字段實現解析器函數,這些函數將從REST API中獲取數據。

現在,讓我們以之前的REST API為例,逐步完成這三個步驟。

步驟 1:分析 REST API 的數據模型

您需要了解的第一件事是不同 REST 終端節點返回的數據的形狀。

在我們的示例場景中,我們可以聲明以下內容:

User 模型有 idname 字段(字符串類型)以及一個表示與 posts 模型的多對關系的 Post字段。

Post 模型具有 idtitlecontent(字符串類型)和 published(布爾類型)字段以及表示與 author 模型的一對一關系的 User 字段。

一旦我們知道了API返回的數據的形狀,我們就可以將我們的發現轉化為GraphQL模式定義語言(SDL):

type User {
id: ID!
name: String!
posts: [Post!]!
}

type Post {
id: ID!
title: String!
content: String!
published: Boolean!
author: User!
}

SDL語法簡潔明了。它允許用字段定義類型。類型上的每個字段也有一個類型。這可以是標量類型,如 IntString,也可以是對象類型,如 PostUser 。字段類型后面的感嘆號表示該字段永遠不能是 null

我們在該模式中定義的類型將成為我們下一步開發的GraphQL API的基礎。

第 2 步:定義 GraphQL 模式

每個GraphQL schema都有三種特殊的根類型:QueryMutationSubscription。這些類型定義了 API 的主要入口點,并可以與 REST 端點進行某種程度的類比(從某種程度上講,REST API 的每個端點可以被視為一個針對其數據的查詢,而在 GraphQL 中,Query 類型上的每個字段都代表了一個可以執行的查詢)。

為了定義 GraphQL schema,我們可以采用以下兩種方法中的一種:

  1. 將每個 REST 端點轉換為相應的查詢
  2. 定制更適合客戶端的 API

在本文中,我們將重點介紹第一種方法,因為它能夠清晰地展示構建 GraphQL schema 的基本步驟。而對于那些細心的讀者來說,通過理解這種方法,可以很容易地推斷出第二種方法的工作原理,并作為一個啟發性的練習。

現在,讓我們從 /users 端點開始。為了將查詢用戶列表的功能添加到我們的 GraphQL API 中,我們需要按照以下步驟操作:

type Query {
users: [User!]!
}

type User {
id: ID!
name: String!
posts: [Post!]!
}

type Post {
id: ID!
title: String!
content: String!
published: Boolean!
author: User!
}

要調用我們剛剛添加到 GraphQL 模式中的 users 查詢,您可以將以下查詢語句放入發送到 GraphQL API 端點的 HTTP POST 請求的主體中:

query {
users {
id
name
}
}

別擔心,稍后我們會向您展示如何實際發送這個查詢。

這里的巧妙之處在于,您在?users?查詢下嵌套的字段決定了哪些字段將包含在服務器響應的 JSON 數據中。這意味著,如果您在客戶端上不需要用戶名,可以簡單地刪除?name?字段。更棒的是,如果您愿意,還可以查詢每個用戶的相關帖子,字段數量完全由您決定,例如:

query {
users {
id
name
posts {
id
title
}
}
}

現在,讓我們將第二個端點 /users/<id> 添加到我們的 API 中。在 REST API 中作為 URL 參數的部分,在 GraphQL 中將變成我們在 Query 類型上引入的字段的參數(為了簡潔,這里省略了 User 和 Post 類型的詳細定義,但它們仍然是模式的一部分):

type Query {
users: [User!]!
user(id: ID!): User
}

下面是一個可能的查詢示例(同樣,我們可以選擇包含?User?類型的任意數量字段,從而決定服務器將返回哪些數據):

query {
user(id: "user-1") {
name
posts {
title
content
}
}
}

至于 /users/<id>/posts 端點,我們實際上并不需要它,因為查詢特定用戶的帖子的能力已經由剛剛添加的 user(id: ID!): User 字段提供了。這真是太棒了!

現在,讓我們通過添加查詢帖子項的功能來完善我們的 API。我們將為所有相關的 REST 端點添加對應的查詢:

type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
post(id: ID!): Post
}

就是這樣,我們現在已經為 GraphQL API 創建了模式定義,它相當于之前的 REST API。接下來,我們需要實現這些模式的解析器函數,以便能夠處理這些查詢并返回相應的數據。

關于突變的說明

在本教程中,我們主要關注了查詢,即從服務器獲取數據的操作。然而,在實際應用中,我們經常需要更改后端存儲的數據。

在使用 REST API 時,我們通常通過向相同的端點發送 PUT、POST 和 DELETE HTTP 請求來完成數據的修改。

而在使用 GraphQL 時,我們則通過 Mutation 根類型來處理數據的變更。以下是一個示例,展示了如何向 API 添加創建、更新和刪除用戶的功能:

type Mutation {
createUser(name: String!): User!
updateUser(id: ID!, name: String!): User
deleteUser(id: ID!): User
}

type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
post(id: ID!): Post
}

請注意,createUserupdateUser 和 deleteUser 這幾個突變操作分別對應于傳統 REST API 中對 /users 端點或 /users/<id> 端點發出的 POST、PUT 和 DELETE 請求。為突變操作實現解析器與處理查詢的解析器非常相似,因此您無需為突變學習全新的知識。它們遵循與查詢相同的機制,唯一的區別在于突變解析器可能會產生副作用,例如修改數據庫中的數據。

步驟 3:為 schema 實現解析器

GraphQL 嚴格區分了架構的結構和行為。

API 的結構由 GraphQL 架構定義語言(SDL)描述。這個架構定義是對 API 功能的抽象描述,它允許客戶端確切地知道它們可以執行哪些操作。

而 GraphQL API 的行為則是通過解析器函數來實現的,這些函數為架構定義中的每個字段提供支持,并知道如何獲取該字段的數據。

實現解析器其實相當簡單。我們只需要調用相應的 REST 端點,并返回我們收到的響應即可:

const baseURL = https://rest-demo-hyxkwbnhaz.now.sh

const resolvers = {
  Query: {
    users: () => {
      return fetch(${baseURL}/users).then(res => res.json())
    },
    user: (parent, args) => {
      const { id } = args
      return fetch(${baseURL}/users/${id}).then(res => res.json())
    },
    posts: () => {
      return fetch(${baseURL}/posts).then(res => res.json())
    },
    post: (parent, args) => {
      const { id } = args
      return fetch(${baseURL}/blog/posts/${id}).then(res => res.json())
    },
  },
}

對于 userpost 解析器,我們還提取了查詢中提供的 id 參數,并將其包含在URL中。

現在,要啟動并運行這個 GraphQL 服務器,你需要使用 graphql-yoga(或其他 GraphQL 服務器庫)來實例化一個服務器,將解析器和架構定義傳遞給它,并調用 start 方法來啟動服務器:

const { GraphQLServer } = require('graphql-yoga')
const fetch = require('node-fetch')

const baseURL = https://rest-demo-hyxkwbnhaz.now.sh const resolvers = { // ... the resolver implementation from above ... } const server = new GraphQLServer({ typeDefs: './src/schema.graphql', resolvers, }) server.start(() => console.log(Server is running on http://localhost:4000))

如果你想跟著這個教程操作,你可以將上面的?index.js?和?schema.graphql?代碼片段復制到你的 Node.js 項目的?src?目錄中相應的文件中,添加?graphql-yoga?作為依賴項,然后運行?node src/index.js?來啟動服務器。

GraphQL Playground 是一個強大的 GraphQL IDE,它允許你以交互方式探索 GraphQL API 的功能。與 Postman 類似,但它提供了許多專為 GraphQL 設計的額外功能。在那里,你可以發送我們之前看到的查詢,并實時查看結果。

查詢將由 GraphQL 服務器的 GraphQL 引擎處理。引擎所需要做的就是為查詢中的每個字段調用相應的解析器,這些解析器會調用適當的 REST 端點來獲取數據。

太棒了!現在,你可以根據需要向查詢中添加 User 類型的字段,并且如果還請求了用戶的相關 Post 項,解析器也會正確地處理它們。

讓我們再次看看user(id: ID)字段的解析器實現:

user: (parent, args) => {
const { id } = args
return fetch(${baseURL}/users/${id}).then(res => res.json()) },

在之前的實現中,我們注意到解析器只返回了從/users/<id>端點接收的數據。由于我們使用的是REST API的平面版本,因此這些數據中并沒有包含與用戶相關聯的帖子(Posts)信息。為了解決這個問題,我們需要為User類型實現一個專用的解析器,來獲取用戶的所有帖子。同樣地,我們也需要為Post類型實現一個解析器,來獲取帖子的作者(User)信息。

下面是更新后的解析器實現:

const resolvers = {
Query: {
// ... the resolver implementation from above ...
},
Post: {
author: parent => {
const { id } = parent
return fetch(${baseURL}/blog/posts/${id}/user).then(res => res.json()) }, }, User: { posts: parent => { const { id } = parent return fetch(${baseURL}/users/${id}/posts).then(res => res.json()) }, }, }

現在,解析器的實現已經能夠處理嵌套查詢了

你已經成功地學會了如何使用GraphQL來包裝現有的REST API,并為其添加關聯數據的處理能力。現在,你可以根據需要擴展你的GraphQL API,以支持更復雜的查詢和數據關系。

高級主題探索:使用 GraphQL 包裝 REST API 的深化理解

在本文中,我們僅初步探討了使用 GraphQL 包裝 REST API 的潛力。為了更全面地理解這一領域,我們接下來將簡要介紹一些在現代 API 開發中至關重要的更高級主題。

認證與授權機制

REST API 通常具備明確的身份驗證和授權流程。每個 API 端點都會設定特定的訪問要求,只有滿足這些要求的客戶端才能成功訪問。在 HTTP 請求中,客戶端通常會攜帶一個身份驗證令牌,用于驗證其身份并獲取訪問權限。

當我們將 REST API 包裝在 GraphQL 之下時,這一流程依然適用。攜帶 GraphQL 查詢的 HTTP 請求同樣需要包含身份驗證令牌。在 GraphQL 服務器處理請求時,它會將這個令牌附加到底層 REST API 調用的相應標頭中,以確保請求能夠成功通過身份驗證和授權檢查。

性能優化策略

在之前的討論中,我們可能已經注意到,雖然 GraphQL 能夠減少客戶端發出的請求數量(從而解決 N+1 請求問題),但 GraphQL 服務器仍然需要執行與客戶端原本需要發出的請求數量相同的 REST 調用。然而,這種轉變仍然帶來了性能上的提升,因為服務器更適合處理這種繁重的工作,特別是在網絡條件不穩定的情況下。

為了進一步提升性能,我們可以引入 Data Loader 模式來實現并行化。通過這一模式,我們可以將解析器調用進行批處理,并使用專用的批處理函數來更高效地檢索數據,這種方法能夠顯著減少數據檢索所需的時間,從而提升整體性能。

實時更新技術

在現代應用程序中,實時更新已經成為了一個重要的功能需求。GraphQL 提供了訂閱功能,允許客戶端訂閱服務器端發生的特定事件。與 GraphQL 查詢和變更操作類似,我們也可以使用 GraphQL 訂閱來包裝實時 API(例如基于 WebSocket 的 API)。這將使客戶端能夠實時接收服務器端的數據更新,從而提供更加流暢和動態的用戶體驗。我們將在后續的文章中深入探討這一話題,敬請期待!

未來展望

在本文中,您學習了如何通過三個簡單的步驟將 REST API 轉換為 GraphQL API:

  1. 分析 REST API 的數據模型
  2. 定義 GraphQL 架構
  3. 實現架構的解析程序

使用 GraphQL 包裝 REST API 是 GraphQL 最具潛力的應用場景之一,盡管這一領域仍處于起步階段。值得注意的是,本文中介紹的過程是手動的。然而,自動化這些步驟才是真正實現這一技術潛力的關鍵所在。因此,我們期待在未來能夠探索更多關于自動化這一過程的想法和解決方案。

如果您希望進一步探索這一領域,您可以考慮查看 graphql-binding-openapi 包。這個包允許您基于 Swagger/Open API 規范自動生成 GraphQL API(以 GraphQL 綁定的形式)。這將為您提供一個更加高效和便捷的方式來將 REST API 轉換為 GraphQL API。

原文鏈接:https://www.prisma.io/blog/how-to-wrap-a-rest-api-with-graphql-8bf3fb17547d

上一篇:

使用NestJS和Prisma構建REST API:處理關系型數據

下一篇:

如何使用 PostgREST 和 Apache APISIX 構建高效、安全的 RESTful API 解決方案
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費