所有文章 > API开发 > 如何用GraphQL封装REST API——3步教程
如何用GraphQL封装REST API——3步教程

如何用GraphQL封装REST API——3步教程

在过去十年中,REST API 一直是构建 Web API 的主流标准。然而,2015 年 Facebook 推出了一种新的开源查询语言 GraphQL,它相较于 REST API 具有显著优势。

从那时起,许多开发人员虽然渴望尝试 GraphQL,但却被现有的 REST API 所牵制。在本文中,我们将展示一个将 REST API 转换为 GraphQL API 的简便流程,无需依赖复杂工具!

REST API 的无架构特性

REST API 的一大短板在于它们缺乏描述 API 端点返回数据结构的具体架构。

假设您正在通过 GET 请求访问这个 REST 端点:/users

这时,您就像是在黑暗中摸索。如果 API 设计者足够明智,您或许可以合理推测它会返回某种用户对象的数组,但每个用户对象具体包含哪些数据,仅凭查看这个端点是无法得知的。

当然,借助 JSON Schema、Swagger/Open API Spec 等工具,可以在一定程度上弥补 REST API 在架构描述上的不足。

而在使用 GraphQL 时,每个 API 的核心都是一个强类型架构,它作为可查询数据结构的明确约定。在接下来的内容中,您将学习如何从 REST API 的 JSON 响应结构中推断并构建 GraphQL 架构。

示例 REST API

在本文中,我们将以一个简单的博客应用为例进行说明。假设我们有两个模型:用户和帖子,它们之间存在一对多的关联关系,即一个用户可以发布多个帖子。

针对这个示例,我们设定了以下终端节点:

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之间的核心差异可以概括为以下两点:

  • REST 拥有一系列端点,每个端点返回的数据结构都是固定的。
  • 相比之下,GraphQL仅通过一个端点就能返回灵活多变的数据结构。

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 中,我们需要按照以下步骤操作:

  • 首先,在 schema 定义中添加 Query 根类型。
  • 然后,在 Query 类型中添加一个返回用户列表的字段。
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

#你可能也喜欢这些API文章!