使用 Express 和 GraphQL 构建简单的 API 服务

GraphQL已成为REST API的一种非常流行的替代方案。 使用GraphQL所获得的灵活性使开发人员可以更轻松地获取应用程序所需的任何信息,以及仅获取应用程序该部分所需的信息。 这给了您非常定制化的API的感觉,并且可以帮助减少带宽。

在本文中,我将向您展示如何使用Node和Express编写自定义GraphQL API。 我还将向您展示如何保护API的某些部分,同时使其他部分向公众开放。

使用Express创建GraphQL API

要创建API,请先创建一个新文件夹并创建一个package.json文件来管理您的依赖项。 您还需要安装一些依赖项才能启动并运行带有Express的GraphQL:

mkdir graphql-express
cd graphql-express
npm init -y
npm install [email protected] [email protected] [email protected] [email protected] [email protected]

现在创建一个名为index.js的文件。 这将是您的主要切入点:

const express = require('express')
const cors = require('cors')
const graphqlHTTP = require('express-graphql')
const gql = require('graphql-tag')
const { buildASTSchema } = require('graphql')

const app = express()
app.use(cors())

const schema = buildASTSchema(gql`
  type Query {
    hello: String
  }
`)

const rootValue = {
  hello: () => 'Hello, world'
}

app.use('/graphql', graphqlHTTP({ schema, rootValue }))

const port = process.env.PORT || 4000
app.listen(port)
console.log(`Running a GraphQL API server at localhost:${port}/graphql`)

这与GraphQL服务器获得的内容一样简单。 当您查询“ hello”时,所有这些操作都会返回“ Hello,world”,但这只是一个开始。 要对其进行测试旋转,请运行node . ,然后在另一个标签中,将浏览器打开到GraphQL Playground 。 在那里,输入http://localhost:4000/graphql以访问您的GraphQL服务器。

 

GraphQL Playground将帮助您探索模式并测试查询。 它甚至会自动为您创建一些文档。

 

尝试使用以下查询查询hello

query {
  hello
}

 

改善您的GraphQL开发人员体验

这里有一些快速提示,可以帮助您改善开发经验:

1.安装一个lint,以帮助捕获编辑器中的错误。 这将有助于保持样式一致并捕获任何易于避免的错误。

要安装StandardJS ,请键入npm install --save-dev [email protected] 。 在您键入时,大多数编辑器将能够向您显示警告和错误。

 

您还可以编辑package.jsonscripts对象,以便可以随时使用npm test

"scripts": {
  "test": "standard"
},

2.进行更改后,自动重新启动服务器。

安装nodemonnpm install --save-dev [email protected]

package.json添加另一个脚本,以便您可以使用npm start运行服务器。 结合以上内容,您的scripts对象应如下所示:

"scripts": {
  "test": "standard",
  "start": "nodemon ."
},

继续并关闭您使用node .运行的服务器node . 然后键入npm start重新启动开发服务器。 从现在开始,您所做的任何更改都会自动重新启动服务器。

创建GraphQL查询

为了获得更多有用的信息,让我们创建一个帖子编辑器。 GraphQL是强类型的,允许您为每个对象创建一个类型并将其连接。 常见的情况是写一个帖子,上面写着一个人写的文字。 更新您的架构以包括这些类型。 您还可以更新Query类型以利用这些新类型。

  type Query {
    posts: [Post]
    post(id: ID): Post
    authors: [Person]
    author(id: ID): Person
  }

  type Post {
    id: ID
    author: Person
    body: String
  }

  type Person {
    id: ID
    posts: [Post]
    firstName: String
    lastName: String
  }

即使未设置解析器,您也可以返回GraphQL Playground并通过单击localhost URL旁的圆形箭头图标来刷新架构。

 

模式浏览器对于弄清楚如何创建查询非常有用。 单击绿色的SCHEMA按钮以签出新架构。

 

您将需要某种方式来存储数据。 为简单起见,请使用JavaScript的Map对象进行内存中存储。 您还可以创建一些类,以帮助将数据从一个对象连接到另一个对象。

const PEOPLE = new Map()
const POSTS = new Map()

class Post {
  constructor (data) { Object.assign(this, data) }
  get author () {
    return PEOPLE.get(this.authorId)
  }
}

class Person {
  constructor (data) { Object.assign(this, data) }
  get posts () {
    return [...POSTS.values()].filter(post => post.authorId === this.id)
  }
}

现在,如果您有一个Person的实例,则只需询问person.posts就可以找到他们的所有帖子。 由于GraphQL只允许您查询所需的数据,因此除非您要获取posts否则getter永远不会被调用,如果这是一项昂贵的操作,则可以加快查询速度。

您还需要更新解析器( rootValue的函数)来适应这些新类型。

const rootValue = {
  posts: () => POSTS.values(),
  post: ({ id }) => POSTS.get(id),
  authors: () => PEOPLE.values(),
  author: ({ id }) => PEOPLE.get(id)
}

很好,但是还没有数据。 现在,存根一些假数据。 您可以在分配给rootValue之后立即添加此函数及其调用。

const initializeData = () => {
  const fakePeople = [
    { id: '1', firstName: 'John', lastName: 'Doe' },
    { id: '2', firstName: 'Jane', lastName: 'Doe' }
  ]

  fakePeople.forEach(person => PEOPLE.set(person.id, new Person(person)))

  const fakePosts = [
    { id: '1', authorId: '1', body: 'Hello world' },
    { id: '2', authorId: '2', body: 'Hi, planet!' }
  ]

  fakePosts.forEach(post => POSTS.set(post.id, new Post(post)))
}

initializeData()

现在您已经设置了所有查询,并添加了一些数据,返回GraphQL Playground并进行一些测试。 尝试获取所有帖子,或获取与每个帖子相关联的所有作者和帖子。

 

或变得怪异并通过id获得单个帖子,然后是该帖子的作者,以及该作者的所有帖子(包括您刚刚查询的帖子)。

 

将用户身份验证添加到Express + GraphQL API

Okta是向项目添加身份验证的一种简单方法。 Okta是一项云服务,允许开发人员创建、编辑和安全地存储用户帐户和用户帐户数据,并将它们与一个或多个应用程序连接。 如果您还没有一个,请注册一个永久免费的开发者帐户 。

您将需要保存一些信息以在应用程序中使用。 创建一个名为.env的新文件。 在其中输入您的组织网址。

HOST_URL=http://localhost:4000
OKTA_ORG_URL=https://{yourOktaOrgUrl}

您还需要一个随机字符串作为会话的“应用程序秘密”。 您可以使用以下命令生成它:

echo "APP_SECRET=`openssl rand -base64 32`" >> .env

接下来,登录到开发人员控制台,导航至“ 应用程序” ,然后单击“ 添加应用程序” 。 选择“ Web” ,然后单击“ 下一步”

 

创建应用程序后进入的页面包含需要保存到.env文件的更多信息。 复制客户ID和客户密码。

OKTA_CLIENT_ID={yourClientId}
OKTA_CLIENT_SECRET={yourClientSecret}

Okta需要的最后一条信息是API令牌。 在开发人员控制台中,导航到API- > 令牌 ,然后单击创建令牌 。 您可以有很多令牌,因此只需给它一个名称即可提醒您它的用途,例如“ GraphQL Express”。 系统会为您提供一个令牌,您现在只能看到它。 如果丢失令牌,则必须创建另一个令牌。 也将此添加到.env

OKTA_TOKEN={yourOktaAPIToken}

创建一个名为okta.js的新文件。 在这里,您将创建一些实用程序功能,并为Okta初始化应用程序。 通过Okta进行身份验证后,您的应用将使用JWT通过访问令牌进行身份验证。 您可以使用它来确定用户是谁。 为了避免直接在您的应用程序中处理身份验证,用户应在Okta的服务器上登录,然后向您发送一个JWT,您可以对其进行验证。

okta.js

const session = require('express-session')

const OktaJwtVerifier = require('@okta/jwt-verifier')
const verifier = new OktaJwtVerifier({
  clientId: process.env.OKTA_CLIENT_ID,
  issuer: `${process.env.OKTA_ORG_URL}/oauth2/default`
})

const { Client } = require('@okta/okta-sdk-nodejs')
const client = new Client({
  orgUrl: process.env.OKTA_ORG_URL,
  token: process.env.OKTA_TOKEN
})

const { ExpressOIDC } = require('@okta/oidc-middleware')
const oidc = new ExpressOIDC({
  issuer: `${process.env.OKTA_ORG_URL}/oauth2/default`,
  client_id: process.env.OKTA_CLIENT_ID,
  client_secret: process.env.OKTA_CLIENT_SECRET,
  redirect_uri: `${process.env.HOST_URL}/authorization-code/callback`,
  scope: 'openid profile'
})

const initializeApp = (app) => {
  app.use(session({
    secret: process.env.APP_SECRET,
    resave: true,
    saveUninitialized: false
  }))
  app.use(oidc.router)
  app.use('/access-token', oidc.ensureAuthenticated(), async (req, res, next) => {
    res.send(req.userContext.tokens.access_token)
  })
}

module.exports = { client, verifier, initializeApp }

initializeApp函数添加了一些中间件,使您可以使用Okta登录。 每当您访问http://localhost:4000/access-token 时,它将首先检查您是否已登录。如果没有登录,它将首先将您发送到Okta的服务器进行身份验证。 身份验证成功后,它将返回到/access-token路由,并打印出您当前的访问令牌,该令牌有效期约为一个小时。

您要导出的client允许您在服务器上运行一些管理调用。 您稍后将使用它根据用户的ID获取有关用户的更多信息。

verifier是您用来验证JWT有效的工具,它为您提供有关用户的一些基本信息,例如其用户ID和电子邮件地址。

现在,在index.js ,您需要导入此文件并调用initializeApp函数。 您还需要使用一个名为dotenv的工具,该工具将读取您的.env文件并将变量添加到process.env 。 在文件的最顶部,添加以下行:

require('dotenv').config({ path: '.env' })

app.use(cors())行之后,添加以下内容:

const okta = require('./okta')
okta.initializeApp(app)

为了使这一切工作,你还需要安装一些新的依赖:

npm i @okta/[email protected] @okta/[email protected] @okta/[email protected] [email protected] [email protected]

现在,您应该可以访问http://localhost:4000/access-token登录并获取访问令牌。 如果您只是在开发人员控制台,则可能会发现自己已经登录。可以注销开发人员控制台,以确保流程正常运行。

创建GraphQL突变

现在是时候使用真实数据了。 可能有一些真正的John和Jane做,但是很可能他们在您的应用程序上还没有帐户。 接下来,我将向您展示如何添加一些变异,这些变异将使用您当前的用户来创建,编辑或删除帖子。

要生成帖子的ID,可以使用uuid 。 使用npm install [email protected]进行npm install [email protected] ,然后使用以下命令将其添加到index.js

const uuid = require('uuid/v4')

那应该在文件顶部附近,在其他require语句旁边。

仍在index.js ,将以下类型添加到架构中:

  type Mutation {
    submitPost(input: PostInput!): Post
    deletePost(id: ID!): Boolean
  }

  input PostInput {
    id: ID
    body: String!
  }

要验证用户并将其另存为新用户,您将需要两个新的实用程序功能。 将它们添加到const rootValue之前:

const getUserId = async ({ authorization }) => {
  try {
    const accessToken = authorization.trim().split(' ')[1]
    const { claims: { uid } } = await okta.verifier.verifyAccessToken(accessToken)

    return uid
  } catch (error) {
    return null
  }
}

const saveUser = async (id) => {
  try {
    if (!PEOPLE.has(id)) {
      const { profile: { firstName, lastName } } = await okta.client.getUser(id)

      PEOPLE.set(id, new Person({ id, firstName, lastName }))
    }
  } catch (ignore) { }

  return PEOPLE.get(id)
}

getUserId函数将检查authorization请求标头是否具有有效令牌。 成功后,它将返回用户的ID。

saveUser函数检查用户是否尚未保存。 如果是,则仅返回缓存的值。 否则,它将获取用户的名字和姓氏并将其存储在PEOPLE对象中。

现在将以下解析器添加到rootValue

  submitPost: async ({ input }, { headers }) => {
    const authorId = await getUserId(headers)
    if (!authorId) return null

    const { id = uuid(), body } = input

    if (POSTS.has(id) && POSTS.get(id).authorId !== authorId) return null
    await saveUser(authorId)

    POSTS.set(id, new Post({ id, authorId, body }))

    return POSTS.get(id)
  },
  deletePost: async ({ id }, { headers }) => {
    if (!POSTS.has(id)) return false

    const userId = await getUserId(headers)
    if (POSTS.get(id).authorId !== userId) return false

    POSTS.delete(id)

    if (PEOPLE.get(userId).posts.length === 0) {
      PEOPLE.delete(userId)
    }

    return true
  }

首先, submitPost变异会检查用户ID,如果没有用户,则返回null 。 这意味着除非您通过身份验证,否则将不会执行任何操作。 然后,它从用户输入中获取idbody 。 如果没有id ,它将生成一个新的id 。 如果已经有具有提供的ID的帖子,它将检查该帖子是否归尝试编辑该帖子的用户所有。 如果不是,则再次返回null

一旦submitPost确定用户能够添加或编辑该帖子,它就会调用saveUser 。 如果用户已经存在, saveUser函数将不执行任何操作,但如果不存在,则将添加用户。 接下来, submitPost将帖子添加到POSTS对象,并在客户端要查询添加的帖子的情况下返回值(例如,获取ID)。

如果您是创建帖子的用户,则deletePost突变仅允许您删除该帖子。 成功删除帖子后,它将检查用户是否还有其他帖子。 如果那是他们唯一的帖子,则deletePost还将从数据集中删除该用户,以清除一些(相当少量的)内存。

现在,您已经可以添加真实数据,也可以摆脱initializeData函数。

测试新的GraphQL突变

尝试打电话给新的变异并创建一个帖子。 由于您未通过身份验证,因此您应该获得null响应。

 

通常,某种类型的应用程序(无论是Web应用程序还是本机应用程序)将处理UI进行身份验证,然后无缝地将Authorization标头传递到API。 在这种情况下,由于我们只关注API,因此我实现了一个端点,用于手动获取auth令牌。

转到http:// localhost:4000 / access-token以使用Okta登录并获取访问令牌。 复制访问令牌,然后返回到GraphQL Playground。 在页面底部,有一个链接,显示HTTP HEADERS 。 当您单击该按钮时,将打开一个部分,可让您添加一些标头作为JSON。 添加以下内容,确保将Bearer添加到令牌的前面,因此它的外观应类似于Bearer eyJraWQ...xHUOjj_A (尽管实际令牌会更长):

{
  "authorization": "Bearer {yourAccessToken}"
}

现在,您应该已通过身份验证,并且同一帖子将返回有效的帖子。

 

如果您想与其他用户打交道,则可以从开发人员控制台通过导航到“ 用户” ->“ 人” ,然后单击“ 添加人”添加人 。 然后,您可以从隐身窗口或退出开发者控制台后访问/access-token端点。

 

了解有关GraphQL、Express和Okta的更多信息

尝试使用API​​一下,看看可以用它做什么有趣的事情。 我想您会很快看到使用GraphQL比传统REST API强大得多的原因,以及即使您只是在使用Playground,使用GraphQL也会很有趣。 查看是否可以建立数据点进行连接,或者从外部来源获取数据。 由于解析器只是async功能,因此您可以轻松地从外部API或数据库中获取数据。 您的想象力是极限。

如果您想查看最终的示例代码,可以在github上找到它。

原文链接: https://www.sitepoint.com/build-a-simple-api-service-with-express-and-graphql/

你可能感兴趣的:(使用 Express 和 GraphQL 构建简单的 API 服务)