发布于 2026-01-06 13 阅读
0

使用 GraphQL 在 Node 应用程序中构建 API DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

在 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
Enter fullscreen mode Exit fullscreen mode

它们分别使用默认设置初始化我们的 Node 项目、安装 GraphQL + Apollo 示例所需的 npm 依赖项以及安装Sequelize CLI 工具

关于依赖关系,我们有:

请注意,Sequelize CLI 工具必须全局安装,否则将无法通过任何命令行界面使用。作为它的第一个命令,我们运行该命令将我们的 Node 项目初始化为 ORM 项目:

sequelize init
Enter fullscreen mode Exit fullscreen mode

它将创建一些与 ORM 框架相关的文件夹,例如modelsconfigmigrations(因为该框架还处理我们数据库的迁移)。

现在,我们来看数据库相关的配置。首先,我们需要一个真正的 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
},
Enter fullscreen mode Exit fullscreen mode

本文仅展示这一development部分,因为这是我们将要讨论的唯一部分。不过,在将应用部署到生产环境之前,请务必更新其他相关部分。

接下来,我们运行以下命令:

sequelize model:generate --name User --attributes login:string,password:string
Enter fullscreen mode Exit fullscreen mode

这是 Sequelize 框架的另一个命令,它会在项目中创建一个新模型——user确切地说,是“模型”。这个模型对我们的身份验证结构至关重要。现在,让我们来看看项目中生成了什么。

目前,我们只创建两个字段:loginpassword。但您可以根据自己的设计需求添加任何其他重要的字段。

您可能还会注意到migrations文件夹下创建了一个新文件。该文件中包含了创建表的代码user。为了将更改迁移到物理数据库,请运行以下命令:

sequelize db:migrate
Enter fullscreen mode Exit fullscreen mode

现在您可以在 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;
Enter fullscreen mode Exit fullscreen mode

有关模式结构的更多详细信息,请参阅此处。简而言之,Query类型 1 用于放置仅返回数据的 API 方法,Mutation类型 2 用于放置创建或更改数据的方法。

其他类型是我们自己的类型,例如 `String`BeerUser`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;
Enter fullscreen mode Exit fullscreen mode

由于解析器基于 Promise,因此其模式本质上是异步的。每个操作都必须具有与模式中定义的完全相同的签名。

请注意,对于所有查询操作,我们都会收到第三个参数:user。该参数将通过context(仍需在配置中index.js)注入。

现在,该jsonwebtoken依赖项将根据提供的凭据对用户进行登录,然后生成正确的 JWT 令牌。此操作将在注册和登录过程中同时发生。

另外,请注意必须为令牌设置过期时间。

最后,我们使用一个JWT_SECRET常量作为 的值secretOrPrivateKey。这个常量与我们在 Express JWT 中间件中用来检查令牌是否有效的密钥相同。

这个常量将被放入一个名为 `.` 的新文件中constants.js。以下是该文件的内容:

const JWT_SECRET = "sdlkfoish23@#$dfdsknj23SD";

module.exports = JWT_SECRET;
Enter fullscreen mode Exit fullscreen mode

请务必将值更改为您自己的安全秘密。唯一的要求是值要足够长。

现在,是时候配置我们的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);
});
Enter fullscreen mode Exit fullscreen mode

如果您使用 Express 作为您的 Web 服务器,则此代码可能看起来很熟悉,只是这里我们设置了两个服务器。

app我们将照常使用Express 。我们会创建它,添加中间件( jwt),然后启动它。不过,ApolloServer可能还需要添加必要的 GraphQL 设置。

ApolloServer它接收 schema(typeDefs)、resolversplaygroundacontext作为参数。该playground属性指定哪个端点将重定向到Prisma 的 GraphQL Playground视图。它是一个内置的 IDE,可以帮助我们测试 GraphQL API。

`_`属性context是一个可选属性,它允许我们在执行 GraphQL 查询/变更之前进行快速转换或验证。在本例中,我们将使用它user从请求中提取对象,并使其可供解析器函数使用。

server对象负责应用中间件,并将该app对象作为参数传递。

就是这样。现在我们来测试一下。使用以下命令运行应用程序:

node index.js
Enter fullscreen mode Exit fullscreen mode

然后,访问该地址http://localhost:3000/graphql,即可显示 Playground 视图。

我们的第一个测试是注册一个新的有效用户。因此,请将以下代码片段粘贴到查询区域,然后点击“执行查询”按钮:

mutation {
  register(login: "john", password: "john")
}
Enter fullscreen mode Exit fullscreen mode

有效的令牌将按如下方式返回:

注册新用户

此令牌已可用于访问敏感方法,例如current

如果您未提供有效的令牌作为 HTTP 标头,则会提示以下错误消息:

未通过身份验证的用户错误

要正确发送,请点击页面底部的“ HTTP 标头”选项卡,并添加以下内容:

{
  "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwibG9naW4iOiJhcHBzaWduYWwiLCJpYXQiOjE1ODk5MTYyNTAsImV4cCI6MTU4OTkxNjQzMH0.bGDmyi3fmEaGf3FNuVBGY7ReqbK-LjD2GmhYCc8Ydts"
}
Enter fullscreen mode Exit fullscreen mode

请务必将Bearer后面的内容替换为您返回的令牌版本。您将得到类似于下图的结果:

正在查询当前用户

显然,如果您已经是注册用户,则可以通过 mutation 登录来获取令牌login

mutation {
  login(login: "appsignal", password: "appsignal")
}
Enter fullscreen mode Exit fullscreen mode

再次提醒,如果您的凭据有误,您将收到相应的错误消息。

我们的啤酒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;
Enter fullscreen mode Exit fullscreen mode

欢迎您补充更多信息。我保留不了解其准确价格的权利。

一旦主要的 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!");
},
Enter fullscreen mode Exit fullscreen mode

它们只是根据给定的参数过滤数据。别忘了导入beersData数组对象:

const beersData = require("./beers");
Enter fullscreen mode Exit fullscreen mode

重启服务器并刷新 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
}
Enter fullscreen mode Exit fullscreen mode

在这种情况下,您需要为每个结果提供确切的啤酒名称。这fragment定义了它将从哪里继承字段,在本例中,是从Beer模式中继承。

简而言之,片段允许您构建一个字段集合,然后将其包含在查询中。别忘了在“查询变量”选项卡中填写 ID:

{
  "id1": 1,
  "id2": 3
}
Enter fullscreen mode Exit fullscreen mode

结果将如下所示:

片段查询示例

请注意,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