在 Node 应用程序中使用 GraphQL 构建 API
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
REST 在 Web 服务领域长期占据主导地位。它易于实现,可以通过 RESTful 模式实现标准化,并且拥有大量支持和简化其开发的库。随后,著名的 API 查询语言 GraphQL 横空出世。
什么是 GraphQL
为了更好地理解 GraphQL,我们需要了解它的定义。GraphQL 的创建目标是:
- 声明式——这意味着你应该有权选择你想要的数据。换句话说,你查询(请求)一些数据,并精确定义你想要获取的内容(这就是模式的作用所在)。
- 它具有组合性——就像许多编程语言的对象一样,你可以让一个字段继承自另一个字段,或者包含在另一个字段中。当然,如果你愿意,也可以同时继承自两者。
- 强类型——一旦字段的类型被定义,就不能再用其他类型了。
- 自文档化——模式本身就提供了很好的文档(包括数据类型、结构、查询和变更等)。
- 更简洁——我们只得到我们请求的内容,这与 REST 有很大不同,REST 会给你一切(这效率不高,尤其是当这一切意味着大量不必要的数据时)。
- 等等。
GraphQL 是一种全新的范式。它引发了这样一个讨论:你的 API 是否应该像我们在后端应用程序中编写数据结构时那样,拥有组织良好、结构清晰的请求和响应数据。
上述 API 缺陷越多,就越表明它需要 GraphQL 来改进。但您不必立即迁移。一些开发者会循序渐进地创建并公开一些端点,然后让客户端使用这些端点。这样,他们可以从客户端和客户端双方收集更多信息,从而判断这是否是正确的方向。
在 Node.js 领域,我们有很多实用工具可以提供帮助。例如,express-graphql就是一款流行的服务器中间件,用于将 GraphQL 与 Node.js 集成。Apollo在 GraphQL API 开发方面也十分便捷。它克服了 express-graphql 的一些缺点,例如难以启用graphql-tools及其模式。我们稍后会详细介绍。
让我们来看看一些实际操作。没有什么比亲眼看到 GraphQL 如何融入常见的 API 示例更好的方法了。为此,我们将创建一个完整的 API 来访问一些啤酒数据。
首先,我们的 API 示例将支持用户注册、登录和身份验证。这样,我们就能确保其安全性,防止未经授权的用户查看我们收藏的啤酒列表。
接下来,我们将深入构建 API 操作,设置 Postgres 数据库来存储凭据和令牌,并对所有内容进行测试。
结束后,我们可以从酒单里选一款啤酒庆祝一下。那么,开始吧。
您知道 AppSignal 已经上线支持 Node 应用了吗?🎉 我们将在未来几周内陆续推出一系列集成功能。请查看文档,了解如何将您的 Node 应用添加到 AppSignal。
项目筹备
我们将要开发的示例需要您已安装Node.js。请确保其版本至少为8.0。
接下来,选择你喜欢的文件夹,然后运行以下命令:
npm init -y
npm i apollo-server-express bcrypt express express-jwt graphql jsonwebtoken pg pg-hstore sequelize
npm install -g sequelize-cli
它们分别使用默认设置初始化我们的 Node 项目、安装 GraphQL + Apollo 示例所需的 npm 依赖项以及安装Sequelize CLI 工具。
关于依赖关系,我们有:
-
apollo-server-express:提供 Express 和 Apollo GraphQL 服务器之间的直接连接。
-
bcrypt:它将用于对我们的密码进行哈希处理。
-
express和express-jwt :Express 框架本身以及用于通过jsonwebtoken模块验证JWT(JSON Web Tokens)的中间件。有很多方法可以处理身份验证过程,但在本文中,我们将使用 JWT 持有者令牌。
-
pg和pg-hstore:Postgres 客户端和 JSON 到 hstore 格式的序列化器/反序列化器(反之亦然)。
-
sequelize:我们将使用 Node.js ORM 来简化与数据库(以及其他数据库)的通信工作。
请注意,Sequelize CLI 工具必须全局安装,否则将无法通过任何命令行界面使用。作为它的第一个命令,我们运行该命令将我们的 Node 项目初始化为 ORM 项目:
sequelize init
它将创建一些与 ORM 框架相关的文件夹,例如models、config和migrations(因为该框架还处理我们数据库的迁移)。
现在,我们来看数据库相关的配置。首先,我们需要一个真正的 Postgres 数据库。如果您还没有安装 Postgres,请先安装。我们将使用pgAdmin作为数据库管理的图形界面工具,并使用它自带的 Web 界面。
接下来,我们将创建示例数据库。为此,请访问 pgAdmin Web 管理界面并创建数据库:
然后,返回项目并按config/config.json如下所示更新内容:
"development": {
"username": "postgres",
"password": "postgres",
"database": "appsignal_graphql_db",
"host": "127.0.0.1",
"dialect": "postgres",
"operatorsAliases": false
},
本文仅展示这一development部分,因为这是我们将要讨论的唯一部分。不过,在将应用部署到生产环境之前,请务必更新其他相关部分。
接下来,我们运行以下命令:
sequelize model:generate --name User --attributes login:string,password:string
这是 Sequelize 框架的另一个命令,它会在项目中创建一个新模型——user确切地说,是“模型”。这个模型对我们的身份验证结构至关重要。现在,让我们来看看项目中生成了什么。
目前,我们只创建两个字段:login和password。但您可以根据自己的设计需求添加任何其他重要的字段。
您可能还会注意到migrations文件夹下创建了一个新文件。该文件中包含了创建表的代码user。为了将更改迁移到物理数据库,请运行以下命令:
sequelize db:migrate
现在您可以在 pgAdmin 中查看结果:
你可能想知道用来存储啤酒数据的表格在哪里。我们不会把它存储在数据库中。原因是我想演示两种方法:从数据库获取数据和从 JavaScript 代码中的静态列表中获取数据。
项目已经准备就绪。现在我们可以开始实现身份验证了。
让我们进行验证!
必须首先实现身份验证,因为如果没有适当的安全措施,就不应该公开任何其他 API 方法。
我们先从模式(schema)开始。GraphQL 模式是 API 客户端必须遵循的规范,才能正确使用 API。它提供了 GraphQL API 可以执行的字段类型、查询和变更的精确层级结构。它是客户端与服务器之间交互的契约。顺便一提,它的条款非常明确且有力。
我们的模式应该放在schema.js文件中。所以,请创建该文件并添加以下内容:
const { gql } = require("apollo-server-express");
const typeDefs = gql`
type User {
id: Int!
login: String!
}
type Beer {
id: Int!
name: String!
brand: String
price: Float
}
type Query {
current: User
beer(id: Int!): Beer
beers(brand: String!): [Beer]
}
type Mutation {
register(login: String!, password: String!): String
login(login: String!, password: String!): String
}
`;
module.exports = typeDefs;
有关模式结构的更多详细信息,请参阅此处。简而言之,Query类型 1 用于放置仅返回数据的 API 方法,Mutation类型 2 用于放置创建或更改数据的方法。
其他类型是我们自己的类型,例如 `String`Beer和User`String`——这些类型是我们创建的,用来反映将在解析器中定义的 JavaScript 模型。
该gql标签用于向编辑器插件(例如Prettier)启用语法高亮显示,有助于保持代码的条理性和组织性。
解析器则是模式中定义的方法的执行器。模式负责 API 的字段、类型和结果,而解析器则以此为参考,实现其背后的执行。
创建一个名为 `<filename>` 的新文件resolvers.js,并添加以下内容:
const { User } = require("./models");
const bcrypt = require("bcrypt");
const jsonwebtoken = require("jsonwebtoken");
const JWT_SECRET = require("./constants");
const resolvers = {
Query: {
async current(_, args, { user }) {
if (user) {
return await User.findOne({ where: { id: user.id } });
}
throw new Error("Sorry, you're not an authenticated user!");
}
},
Mutation: {
async register(_, { login, password }) {
const user = await User.create({
login,
password: await bcrypt.hash(password, 10),
});
return jsonwebtoken.sign({ id: user.id, login: user.login }, JWT_SECRET, {
expiresIn: "3m",
});
},
async login(_, { login, password }) {
const user = await User.findOne({ where: { login } });
if (!user) {
throw new Error(
"This user doesn't exist. Please, make sure to type the right login."
);
}
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
throw new Error("You password is incorrect!");
}
return jsonwebtoken.sign({ id: user.id, login: user.login }, JWT_SECRET, {
expiresIn: "1d",
});
},
},
};
module.exports = resolvers;
由于解析器基于 Promise,因此其模式本质上是异步的。每个操作都必须具有与模式中定义的完全相同的签名。
请注意,对于所有查询操作,我们都会收到第三个参数:user。该参数将通过context(仍需在配置中index.js)注入。
现在,该jsonwebtoken依赖项将根据提供的凭据对用户进行登录,然后生成正确的 JWT 令牌。此操作将在注册和登录过程中同时发生。
另外,请注意必须为令牌设置过期时间。
最后,我们使用一个JWT_SECRET常量作为 的值secretOrPrivateKey。这个常量与我们在 Express JWT 中间件中用来检查令牌是否有效的密钥相同。
这个常量将被放入一个名为 `.` 的新文件中constants.js。以下是该文件的内容:
const JWT_SECRET = "sdlkfoish23@#$dfdsknj23SD";
module.exports = JWT_SECRET;
请务必将值更改为您自己的安全秘密。唯一的要求是值要足够长。
现在,是时候配置我们的index.js文件了。请将其内容替换为以下内容:
const express = require("express");
const { ApolloServer } = require("apollo-server-express");
const jwt = require("express-jwt");
const typeDefs = require("./schema");
const resolvers = require("./resolvers");
const JWT_SECRET = require("./constants");
const app = express();
const auth = jwt({
secret: JWT_SECRET,
credentialsRequired: false,
});
app.use(auth);
const server = new ApolloServer({
typeDefs,
resolvers,
playground: {
endpoint: "/graphql",
},
context: ({ req }) => {
const user = req.headers.user
? JSON.parse(req.headers.user)
: req.user
? req.user
: null;
return { user };
},
});
server.applyMiddleware({ app });
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log("The server started on port " + PORT);
});
如果您使用 Express 作为您的 Web 服务器,则此代码可能看起来很熟悉,只是这里我们设置了两个服务器。
app我们将照常使用Express 。我们会创建它,添加中间件( jwt),然后启动它。不过,ApolloServer可能还需要添加必要的 GraphQL 设置。
ApolloServer它接收 schema(typeDefs)、resolvers和playgroundacontext作为参数。该playground属性指定哪个端点将重定向到Prisma 的 GraphQL Playground视图。它是一个内置的 IDE,可以帮助我们测试 GraphQL API。
`_`属性context是一个可选属性,它允许我们在执行 GraphQL 查询/变更之前进行快速转换或验证。在本例中,我们将使用它user从请求中提取对象,并使其可供解析器函数使用。
该server对象负责应用中间件,并将该app对象作为参数传递。
就是这样。现在我们来测试一下。使用以下命令运行应用程序:
node index.js
然后,访问该地址http://localhost:3000/graphql,即可显示 Playground 视图。
我们的第一个测试是注册一个新的有效用户。因此,请将以下代码片段粘贴到查询区域,然后点击“执行查询”按钮:
mutation {
register(login: "john", password: "john")
}
有效的令牌将按如下方式返回:
此令牌已可用于访问敏感方法,例如current。
如果您未提供有效的令牌作为 HTTP 标头,则会提示以下错误消息:
要正确发送,请点击页面底部的“ HTTP 标头”选项卡,并添加以下内容:
{
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwibG9naW4iOiJhcHBzaWduYWwiLCJpYXQiOjE1ODk5MTYyNTAsImV4cCI6MTU4OTkxNjQzMH0.bGDmyi3fmEaGf3FNuVBGY7ReqbK-LjD2GmhYCc8Ydts"
}
请务必将Bearer后面的内容替换为您返回的令牌版本。您将得到类似于下图的结果:
显然,如果您已经是注册用户,则可以通过 mutation 登录来获取令牌login:
mutation {
login(login: "appsignal", password: "appsignal")
}
再次提醒,如果您的凭据有误,您将收到相应的错误消息。
我们的啤酒API
为了简化起见,我们不会在数据库中创建 Beer 域名。一个单独的 JS 文件就能完成这项工作。但我建议您也迁移到我们的 ORM 模型,并充分利用您目前掌握的知识。
那么,我们就从这里开始吧。这是我们文件的代码beers.js(请确保也创建了该文件):
var beersData = [
{
id: 1,
name: "Milwaukee's Best Light",
brand: "MillerCoors",
price: 7.54,
},
{
id: 2,
name: "Miller Genuine Draft",
brand: "MillerCoors",
price: 6.04,
},
{
id: 3,
name: "Tecate",
brand: "Heineken International",
price: 3.19,
},
];
module.exports = beersData;
欢迎您补充更多信息。我保留不了解其准确价格的权利。
一旦主要的 GraphQL 设置结构搭建完成,添加新操作就非常容易了。我们只需要用新操作更新 schema(我们已经完成了这一步),并将相应的函数添加到 schema 中即可resolvers.js。
以下是新增的查询语句:
async beer(_, { id }, { user }) {
if (user) {
return beersData.filter((beer) => beer.id == id)[0];
}
throw new Error("Sorry, you're not an authenticated user!");
},
async beers(_, { brand }, { user }) {
if (user) {
return beersData.filter((beer) => beer.brand == brand);
}
throw new Error("Sorry, you're not an authenticated user!");
},
它们只是根据给定的参数过滤数据。别忘了导入beersData数组对象:
const beersData = require("./beers");
重启服务器并刷新 Playground 页面。请注意,我们也已将这些新查询优化为安全查询,因此您需要提供有效的令牌作为请求头。
这是按品牌查询的结果:
在这个调用中,我们使用了查询变量。它允许你通过动态提供参数来调用 GraphQL 查询。当有其他应用程序(而不仅仅是单个 Web IDE)调用 GraphQL API 时,它非常有用。
这就是 GraphQL 的魅力所在。它允许更复杂的查询组合。例如,假设我们需要在一次调用中查询两种特定的啤酒,并根据 ID 列表进行筛选。
目前,我们仅支持按单个 ID 或单个品牌名称进行筛选,不支持按参数列表进行筛选。
GraphQL 并没有直接实现一个新的查询函数,而是提供了一个名为Fragments的功能。看看我们的查询语句是什么样的:
query getBeers($id1: Int!, $id2: Int!) {
beer1: beer(id: $id1) {
...beerFields
}
beer2: beer(id: $id2) {
...beerFields
}
}
fragment beerFields on Beer {
id
name
brand
price
}
在这种情况下,您需要为每个结果提供确切的啤酒名称。这fragment定义了它将从哪里继承字段,在本例中,是从Beer模式中继承。
简而言之,片段允许您构建一个字段集合,然后将其包含在查询中。别忘了在“查询变量”选项卡中填写 ID:
{
"id1": 1,
"id2": 3
}
结果将如下所示:
请注意,Authorization标头也在那里,只是隐藏在选项卡中。
结论
虽然花了不少时间,但我们最终完成了。现在您拥有了一个功能齐全的 GraphQL API,它能够提供查询和变更操作,更重要的是,它以安全的方式运行。
这里还有很多可以添加的内容。例如,将 Beer 的模型迁移到直接从 Postgres 存储和获取数据,插入一些日志以便更好地了解发生了什么,以及对主模型添加一些变更操作。
Apollo + Express + GraphQL 已被证明是构建强大且快速的 Web API 的绝佳组合。要了解更多信息,请务必访问http://graphql.org/learn/。非常棒的资源!
PS:如果您喜欢这篇文章,请订阅我们全新的 JavaScript Sorcery 邮件列表,每月深入探讨更多神奇的 JavaScript 技巧和窍门。
PPS 如果您想要一个适用于 Node 的一体化 APM,或者您已经熟悉 AppSignal,请去看看适用于 Node.js 的 AppSignal。
迪奥戈·索萨十多年来一直热衷于编写简洁代码、软件设计和开发。如果他不在编程或撰写相关文章,你通常会发现他在看动画片。
文章来源:https://dev.to/appsignal/building-apis-with-graphql-in-your-node-application-118c






