原文地址:How to Create a Secure Node.js GraphQL API 作者:Marcos
本文的目的是提供一份快速指南 -- 《如何快速在如何在 Node.js 中创建安全的 GraphQL API》。
可能会想到有以下的问题:
- 使用 GraphQL API 的目的是什么?
- 什么是 GraphQL API?
- 什么是 GraphQL 查询?
- GraphQL 有什么好处?
- GraphQL 比 REST 更好吗?
- 为什么使用 Node.js?
这些问题都非常直面人心,在回答这些问题之前,我们先简单概述下 Web 开发的现状:
- 你会发现现在所有的解决方案都是围绕使用某种 API 来实现。
- 即使您只使用像 Facebook 或 Instagram 这样的社交网络,你也是会链接到使用 API 的前端。
- 如果你再深入了解一点,你会发现几乎所有在线服务都在使用不同类型的 API ,包括 Netflix,Spotify 和 YouTube 等。
实际上,在这些场景中,你都会发现有些 API 你并不需要详细了解它。比如,你不需要知道他们是如何构建的,也不需要在自己的系统中使用和它们一样的技术。API 只在意服务端与客户端之间的通信的方式,而不会依赖于特定的技术栈。
怎么定义一个 API 是否良好?它可能会拥有可靠的、可维护的和可扩展的 API,以及可以为多种客户端和前端应用程序提供服务。
那什么是 GraphQL API?
GraphQL 是一种 APIs 查询语言,开发用于 Facebook 内部使用,并在 2015 年发布供公众使用。它支持读取、写入和实时更新。同时它也是开源的,常常被用来和 REST 或者其他的架构来比较。简而言之,它基于两部分:
- GraphQL Queries(查询):允许客户端进行读取和操作,并可以指定数据的接收格式
- GraphQL Mutations(变更):向服务端写入数据,可以约定数据的写入方式。
尽管本文应该以一个真实简单的场景来演示说明如何构建和使用 GraphQL APIs,但我们不会对 GraphQL 进行详细的介绍。原因很简单,因为 GraphQL 官方团队提供的文档非常全,并且在 Introduction to GraphQL 列出了几个最佳实践。
什么是 GraphQL 查询?
如前面所讲述的那样,查询 (query) 是客户端从 API 读取和操作数据的方式。你可以传递一个对象的类型,并且定义所希望返回的字段类型。下面是一个简单的查询:
代码语言:javascript复制query{
users{
firstName,
lastName
}
}
在这个查询中,我们想从用户集合中获取所有的用户,但只需要返回 firstName
和 lastName
。查询结果将会像下面这样:
{
"data": {
"users": [
{
"firstName": "Marcos",
"lastName": "Silva"
},
{
"firstName": "Paulo",
"lastName": "Silva"
}
]
}
}
这对于客户端使用来说非常简单。
使用 GraphQL API 的目的是什么?
构建 API 的目的是希望能将软件作为一种服务,并可以被其他外部服务集成。即使你的这个应用只提供给了一个前端使用,你也可以将这个前端视为一个外部服务。所以当两者通过 API 的形式来进行通信时,其他项目也可以使用同样的方式来工作。
如果你在一个大团队中工作,可以将它拆分成前端和后端两个团队,这样他们就可以使用相同的技术栈来工作从而提高效率。在构建 API 时,选择更接近实际需求的解决方案非常重要。
在本文中,我们将关注与怎么使用框架来构建 GraphQL API。
GraphQL 比 REST 更好吗?
这个问题可能会有点「引战」,但不得不说,这得看情况来选择。
GraphQL 在一些场景中非常适合。REST 是一种架构设计模式,在很多场景中也得到了验证。如今,有大量的文章试图证明为什么一个比另一个好,或者你应该使用 REST 而不是 GraphQL。此外,有很多方法在内部使用 GraphQL,并仍然用 REST 来维护你的 API。
最好的方法是去了解每种方法的优点,分析可使用的解决方案,评估团队使用这种解决方案的舒适程度,并且评估你和你的团队成员是否可以快速上手。
这篇文章更多的是一个实用指南,而不是对 GraphQL 和 REST 进行主观比较。如果你想了解这两者的详细区别,我建议你可以看看我的另一篇文章: GraphQL vs. REST - A GraphQL Tutorial。
为什么使用 Node.js?
GraphQL 有几种不同的库可供我们实用。出于本文的目的,我们决定实用 JavaScript 和 Node.js,因为它们被广泛地使用,并且 Node.js 允许开发者使用熟悉的前端语言来进行服务端开发。
将我们的构建方式和基于 REST 的 API 进行比较非常有用,类似另外一篇文章那样:Creating a Secure REST API in Node.js. 这篇文章还展示了如何使用 Node.js 和 Express 来开发 REST API 框架,你可以在这两种方法中找出一些差异。Node.js 还设计了一些可扩展的网络应用程序,包括一个全球性的社区以及几个开源库,你可以在 npm 上找到他们。
接下来,我们将演示如何使用 GraphQL、Node.js 和 Express 来构建 API !
准备开始 GraphQL
我们会先为 GraphQL API 提供一个构思。在这之前,你需要了解 Node.js 和 Express 的基础知识。本文的所有示例可以在这个链接中获得:https://github.com/makinhs/node-graphql-tutorial
我们将会处理两种类型的资源(两个集合):
- Users:用来展示如何进行基本的 CRUD。
- Products:通过一些细节来展示 GraphQL 的更多功能。
Users 的数据结构如下:
- id
- firstname
- lastname
- password
- permissionLevel
Products 的数据结构如下:
- id
- name
- description
- price
至于语言版本,我们将会在这个项目中使用 TypeScript。在源文件中,你可以使用 TypeScript 来修改所有的内容。
Let’s Code!
首先,确保你的 Node.js 版本是最新的。撰写本文时,Node.js 当前的版本为 10.15.3。
初始化项目
我们先创建一个名为 node-graphql
的文件夹。然后我们打开一个终端或者 git 控制台,并使用 npm init
来初始化。
配置项目依赖和 TypeScript
为了加快这一步,你可以直接使用我们 git 仓库中的内容来替换你的 package.json
,这里面包含了所需的所有依赖:
{
"name": "node-graphql",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"tsc": "tsc",
"start": "npm run tsc && node ./build/app.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"@types/express": "^4.16.1",
"@types/express-graphql": "^0.6.2",
"@types/graphql": "^14.0.7",
"express": "^4.16.4",
"express-graphql": "^0.7.1",
"graphql": "^14.1.1",
"graphql-tools": "^4.0.4"
},
"devDependencies": {
"tslint": "^5.14.0",
"typescript": "^3.3.4000"
}
}
更新完 package.json
后,在终端中继续输入 npm install
。它将会为你安装 GraphQL API、Express 的所有依赖。
下一步是配置 TypeScript 的编译模式,我们在项目根目录下创建一个 tsconfig.json
,并输入以下内容:
{
"compilerOptions": {
"target": "ES2016",
"module": "commonjs",
"outDir": "./build",
"strict": true,
"esModuleInterop": true
}
}
这个配置将会在当前应用文件夹中生效,我们可以创建一个 app.ts
,并输入一些调试代码:
console.log('Hello Graphql Node API tutorial');
通过我们的配置,现在你可以在终端中运行 npm start
,等待构建完成后会发现代码正常执行。在控制台中应该会看到我们打印的内容 Hello Graphql Node API tutorial
。在后台,会根据 tsconfig.json
来将 TypeScript 编译成纯 JavaScript,然后会执行 build
文件夹中的构建结果。
现在我们来构建 GraphQL API 的基本内容,首先我们先导入以下依赖库:
代码语言:javascript复制import express from 'express';
import graphqlHTTP from 'express-graphql';
import { makeExecutableSchema } from 'graphql-tools';
下一步是在 Express 中处理我们的应用逻辑和基本的 GraphQL 配置,例如:
代码语言:javascript复制import express from 'express';
import graphqlHTTP from 'express-graphql';
import { makeExecutableSchema } from 'graphql-tools';
const app: express.Application = express();
const port = 3000;
let typeDefs: any = [`
type Query {
hello: String
}
type Mutation {
hello(message: String) : String
}
`];
let helloMessage: String = 'World!';
let resolvers = {
Query: {
hello: () => helloMessage
},
Mutation: {
hello: (_: any, helloData: any) => {
helloMessage = helloData.message;
return helloMessage;
}
}
};
app.use(
'/graphql',
graphqlHTTP({
schema: makeExecutableSchema({typeDefs, resolvers}),
graphiql: true
})
);
app.listen(port, () => console.log(`Node Graphql API listening on port ${port}!`));
在上面的代码中,我们做了以下事情:
- 启动 Express 服务并监听 3000 端口
- 定义这个例子会使用到的 queries 和 mutations
- 定义 queries 和 mutations 如何工作
好,那 typeDefs 和 resolvers 是什么?queries 和 mutations 之间有什么关系?
typeDefs
:定义 queries 和 mutations 的数据结构resolvers
:在这里定义了 queries 和 mutations 是如何工作的,而不是定义所期望的字段Queries(查询)
:我们要从服务器获取的内容Mutations(变更)
:请求将会改变服务器中的数据
现在,我们重新执行一下 npm start
,我们可以看到在控制台中显示了以下消息:Node Graphql API listening on port 3000!
现在我们可以尝试通过以下方式来调试我们的 GraphQL 应用程序:
http://localhost:3000/graphql
toptal-blog-image-1556642146731-899e2ac152384d9eb080d40467351d7c.png
很好,我们现在可以写我们的第一个查询了:
toptal-blog-image-1556642154647-6d4f0a1557da7ba03eb98f1daa8e451a.png
需要注意的是要按照我们在 typeDefs
中定义的方式,这个页面可以帮我们构建查询。
那么我们怎么才能改变这个值呢?可以用 Mutations!
现在,我们来看看当我们用一个 mutation(变更) 来改变内存里的一个值会发安生什么:
toptal-blog-image-1556642161175-73ed2ee503125a93d3dbc04d9e292407.png
我们现在可以通过 GraphQL Node.js API 来做基本的 CRUD 了。接下来我们继续。
Products
我们将使用一个名为 Products
的模块。为了简化本文的篇幅,我们将使用内存数据库来进行演示。接下来我们将定义 Products
的模型(model)和服务(service)。
模型(model)定义如下:
代码语言:javascript复制export class Product {
private id: Number = 0;
private name: String = '';
private description: String = '';
private price: Number = 0;
constructor(productId: Number,
productName: String,
productDescription: String,
price: Number) {
this.id = productId;
this.name = productName;
this.description = productDescription;
this.price = price;
}
}
和 GraphQL 进行通信的服务(service)定义如下:
代码语言:javascript复制export class ProductsService {
public products: any = [];
configTypeDefs() {
let typeDefs = `
type Product {
name: String,
description: String,
id: Int,
price: Int
} `;
typeDefs = `
extend type Query {
products: [Product]
}
`;
typeDefs = `
extend type Mutation {
product(name:String, id:Int, description: String, price: Int): Product!
}`;
return typeDefs;
}
configResolvers(resolvers: any) {
resolvers.Query.products = () => {
return this.products;
};
resolvers.Mutation.product = (_: any, product: any) => {
this.products.push(product);
return product;
};
}
}
Users
同样的,我们将遵循前面的规则来定义 Users。模型的定义:
代码语言:javascript复制export class User {
private id: Number = 0;
private firstName: String = '';
private lastName: String = '';
private email: String = '';
private password: String = '';
private permissionLevel: Number = 1;
constructor(id: Number,
firstName: String,
lastName: String,
email: String,
password: String,
permissionLevel: Number) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.password = password;
this.permissionLevel = permissionLevel;
}
}
同时,服务的定义如下:
代码语言:javascript复制const crypto = require('crypto');
export class UsersService {
public users: any = [];
configTypeDefs() {
let typeDefs = `
type User {
firstName: String,
lastName: String,
id: Int,
password: String,
permissionLevel: Int,
email: String
} `;
typeDefs = `
extend type Query {
users: [User]
}
`;
typeDefs = `
extend type Mutation {
user(firstName:String,
lastName: String,
password: String,
permissionLevel: Int,
email: String,
id:Int): User!
}`;
return typeDefs;
}
configResolvers(resolvers: any) {
resolvers.Query.users = () => {
return this.users;
};
resolvers.Mutation.user = (_: any, user: any) => {
let salt = crypto.randomBytes(16).toString('base64');
let hash = crypto.createHmac('sha512', salt).update(user.password).digest("base64");
user.password = hash;
this.users.push(user);
return user;
};
}
}
小提示:源代码可以从这个链接中获得。
现在我们可以运行和测试我们的代码了。执行 npm start
,我们将在 3000 端口运行我们的服务器。我们可以通过这个地址来访问 GraphQL 进行调试: http://localhost:3000/graphql 。
我们试一下用 mutation 来将一个 item 添加到 Product 列表中:
toptal-blog-image-1556642172745-bf28c918810cce73d3fae42d55bc1aa2.png
为了验证是否正常,我们来对 Products 进行查询,但我们只希望返回 id
,name
和 price
:
query{
products{
id,
name,
price
}
}
查询结果如下:
{
"data": {
"products": [
{
"id": 100,
"name": "My amazing product",
"price": 400
}
]
}
}
上面的代码的执行结果会如你所愿。你还可以更改你所需要的字段,比如你希望添加 description
:
query{
products{
id,
name,
description,
price
}
}
我们试下对 Users 发起变更:
代码语言:javascript复制mutation{
user(id:200,
firstName:"Marcos",
lastName:"Silva",
password:"amaz1ingP4ss",
permissionLevel:9,
email:"marcos.henrique@toptal.com") {
id
}
}
然后进行查询:
代码语言:javascript复制query{
users{
id,
firstName,
lastName,
password,
email
}
}
会得到如下内容:
代码语言:javascript复制{
"data": {
"users": [
{
"id": 200,
"firstName": "Marcos",
"lastName": "Silva",
"password": "kpj6Mq0tGChGbZ BT9Nw6RMCLReZEPPyBCaUS3X23lZwCCp1Ogb94/oqJlya0xOBdgEbUwqRSuZRjZGhCzLdeQ==",
"email": "marcos.henrique@toptal.com"
}
]
}
}
到现在,我们的 GraphQL 的基本框架已经搭好了!如果还要继续构建成一个有用的、功能全的 API 还有许多工作要做,但现在基本的核心已经搭好了。
总结与最后的想法
即使已经尽量减少篇幅了,这篇文章还是很长,因为包含了许多关于开发 GraphQL Node.js API 的基本信息。
我们来回顾一下本文的内容:
- 使用 Node Express 和 GraphQL 来构建 GraphQL API;
- GraphQL 的基本使用;
- 查询 (Query) 和变更 (Mutation) 的基本使用;
- 创建模块 (Module) 的基本方法;
- 测试我们的 GraphQL API;
为了将内容侧重于开发使用,本文忽略了开发中一些重要的内容,简单总结如下:
- 新增内容时需要校验
- 对服务中的错误进行正确处理
- 校验用户在每个请求中所使用的字段
- 添加一个 JWT 拦截器来保护 API 接口
- 用更有效的加密算法来处理密码
- 添加单元和集成测试
请记住,我们在 Git 上有完整的源代码。请随意使用、fork、提 issue 和 PR。请注意,本文中所提到所有标准和建议都不会是一成不变的。
这只是许多构建 GraphQL API 方法中的一种。另外,一定要详细地阅读和探索学习 GraphQL,并了解它能给我们带来什么,怎么可以让我们的 API 接口设计地更好。