用Node.js建立安全的 GraphQL API

前端先鋒發表於2019-05-07

翻譯:瘋狂的技術宅 www.toptal.com/graphql/gra…

本文的目標是提供關於如何建立安全的 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 是一種 API 所使用的查詢語言,由Facebook開發並用於其內部專案,並於2015年公開發布。它支援讀取、寫入和實時更新等操作。同時它也是開源的,通常會與REST和其他架構放在一起進行比較。簡而言之,它基於:

  • GraphQL查詢 —— 允許客戶端進行讀取和控制接收資料的方式。
  • GraphQL 修改 —— 描述怎樣在伺服器上寫入資料。關於怎樣將資料寫入系統的GraphQL約定。

雖然本文應該展示一個關於如何構建和使用GraphQL API的簡單但真實的場景,但我們不會去詳細介紹GraphQL。因為GraphQL團隊提供了全面的文件,並在Introduction to GraphQL中列出了幾個最佳實踐。

什麼是GraphQL查詢?

如上所述,查詢是客戶端從API讀取和運算元據的一種方式。你可以傳遞物件的型別,並選擇要接收的欄位型別。下面是一個簡單的查詢:

query{
  users{
    firstName,
    lastName
  }
}
複製程式碼

我們嘗試從使用者庫中查詢所有使用者,但只接收firstNamelastName。此查詢的結果將類似於:

{
  "data": {
    "users": [
      {
        "firstName": "Marcos",
        "lastName": "Silva"
      },
      {
        "firstName": "Paulo",
        "lastName": "Silva"
      }
    ]
  }
}
複製程式碼

客戶端的使用非常簡單。

使用GraphQL API的目的是什麼?

建立API的目的是使自己的軟體具有可以被其他外部服務整合的能力。即使你的程式被單個前端程式所使用,也可以將此前端視為外部服務,為此,當通過API為兩者之間提供通訊時,你能夠在不同的專案中工作。

如果你在一個大型團隊中工作,可以將其拆分為建立前端和後端團隊,從而允許他們使用相同的技術,並使他們的工作更輕鬆。

在本文中,我們將重點介紹怎樣構建使用GraphQL API的框架。

GraphQL比REST更好嗎?

GraphQL是一種適合多種情況的方法。 REST是一種體系結構方法。如今,有大量的文章可以解釋為什麼一個比另一個好,或者為什麼你應該只使用REST而不是GraphQL。另外你可以通過多種方式在內部使用GraphQL,並將API的端點維護為基於REST的架構。

你應該做的是瞭解每種方法的好處,分析自己正在建立的解決方案,評估你的團隊使用解決方案的舒適程度,並評估你是否能夠指導你的團隊快速掌握這些技術。

本文更偏重於實用指南,而不是GraphQL和REST的主觀比較。如果你想檢視這兩者的詳細比較,我建議你檢視我們的另一篇文章,為什麼GraphQL是API的未來

在今天的文章中,我們將專注於怎樣用Node.js建立GraphQL API。

為什麼要使用Node.js?

GraphQL有好幾個不同的支援庫可供使用。出於本文的目的,我們決定使用Node.js環境下的庫,因為它的應用非常廣泛,並且Node.js允許開發人員使用他們熟悉的前端語法進行伺服器端開發。

掌握GraphQL

我們將為自己的 GraphQL API 設計一個構思的框架,在開始之前,你需要了解Node.js和Express的基礎知識。這個GraphQL示例專案的原始碼可以在這裡找到(github.com/makinhs/nod…

我們將會處理兩種型別的資源:

  • Users ,處理基本的CRUD。
  • Products, 我們對它的介紹會詳細一點,以展示GraphQL更多的功能。

Users 包含以下欄位:

  • id
  • firstname
  • lastname
  • email
  • password
  • permissionLevel

Products 包含以下欄位:

  • id
  • name
  • description
  • price

至於編碼標準,我們將在這個專案中使用TypeScript。

讓我們開始編碼!

首先,要確保安裝了最新的Node.js版本。在本文釋出時,在Nodejs.org上當前版本為10.15.3。

初始化專案

讓我們建立一個名為node-graphql的新資料夾,並在終端或Git CLI控制檯下使用以下命令: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

接著是配置我們的TypeScript模式。在根資料夾中建立一個名為tsconfig.json的檔案,其中包含以下內容:

{
  "compilerOptions": {
    "target": "ES2016",
    "module": "commonjs",
    "outDir": "./build",
    "strict": true,
    "esModuleInterop": true
  }
}
複製程式碼

這個配置的程式碼邏輯將會出現在app資料夾中。在那裡我們可以建立一個app.ts檔案,在裡面新增以下程式碼用於基本測試:

console.log('Hello Graphql Node API tutorial');
複製程式碼

通過前面的配置,現在我們可以執行 npm start 進行構建和測試了。在終端控制檯中,你應該能夠看到輸出的字串“Hello Graphql Node API tutorial”。在後臺場景中,我們的配置會將 TypeScript 程式碼編譯為純 JavaScript,然後在build資料夾中執行構建。

現在為GraphQL API配置一個基本框架。為了開始我們的專案,將新增三個基本的匯入:

  • Express
  • Express-graphql
  • Graphql-tools

把它們放在一起:

import express from 'express';
import graphqlHTTP from 'express-graphql';
import {makeExecutableSchema} from 'graphql-tools';
複製程式碼

現在應該能夠開始編碼了。下一步是在Express中處理我們的程式和基本的GraphQL配置,例如:

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。
  • 定義我們想要用作快速示例的查詢和修改。
  • 定義查詢和修改的工作方式。

好的,但是typeDefs和resolvers中發生了什麼,它們與查詢和修改的關係又是怎樣的呢?

  • typeDefs - 我們可以從查詢和修改中獲得的模式的定義。
  • Resolvers - 在這裡我們定義了查詢和修改的功能和行為,而不是想要的欄位或引數。
  • Queries - 我們想要從伺服器讀取的“獲取方式”。
  • Mutations - 我們的請求將會影響在自己的伺服器上的資料。

現在讓我們再次執行npm start,看看我們能得到些什麼。我們希望該程式執行後產生這種效果:Graphql API 偵聽3000埠。

我們現在可以試著通過訪問 http://localhost:3000/graphql 查詢和測試GraphQL API:

伺服器測試

好了,現在可以編寫第一個自己的查詢了,先定義為“hello”。

第一次查詢

請注意,我們在typeDefs中定義它的方式,頁面可以幫助我們構建查詢。

這很好,但我們怎樣才能改變值呢?當然是mutation!

現在,讓我們看看當我們用mutation對值進行改變時會發生什麼:

mutation 演示

現在我們可以用GraphQL Node.js API進行基本的CRUD操作了。接下來開始使用這些程式碼。

Products

對於Products,我們將使用名為products的模組。為了是本文不那麼囉嗦,我們將用記憶體資料庫進行演示。先定義一個模型和服務來管理Products。

我們的模型將基於以下內容:

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通訊的服務定義為:

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,我們將遵循與products模組相同的結構。我們將為使用者提供模型和服務。該模型將定義為:

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;
    }

}
複製程式碼

同時,我們的服務將會是這樣:

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;
        };

    }

}
複製程式碼

提醒一下,原始碼可以在 github.com/makinhs/nod… 找到。

現在執行並測試我們的程式碼。執行npm start,將在埠3000上執行伺服器。我們現在可以通過訪問http://localhost:3000/graphql來測試自己的GraphQL

嘗試一個mutation,將一個專案新增到我們的product列表中:

GraphQL mutation 演示

為了測試它是否有效,我們現在使用查詢,但只接收idnameprice

query{
  products{
    id,
    name,
    price
  }
}

將會返回:
{
  "data": {
    "products": [
          {
        "id": 100,
        "name": "My amazing product",
        "price": 400
      }
    ]
  }
}
複製程式碼

很好,按照預期工作了。現在可以根據需要獲取欄位了。你可以試著新增一些描述:

query{
  products{
    id,
    name,
    description,
    price
  }
}
複製程式碼

現在我們可以對product進行描述。接下來試試user吧。

mutation{
  user(id:200,
  firstName:"Marcos",
  lastName:"Silva",
  password:"amaz1ingP4ss",
  permissionLevel:9,
  email:"marcos.henrique@toptal.com") {
    id
  }
}
複製程式碼

查詢如下:

query{
  users{
    id,
    firstName,
    lastName,
    password,
    email
  }
}
複製程式碼

返回內容如下:

{
  "data": {
    "users": [
      {
        "id": 200,
        "firstName": "Marcos",
        "lastName": "Silva",
        "password": "kpj6Mq0tGChGbZ+BT9Nw6RMCLReZEPPyBCaUS3X23lZwCCp1Ogb94/oqJlya0xOBdgEbUwqRSuZRjZGhCzLdeQ==",
        "email": "marcos.henrique@toptal.com"
      }
    ]
  }
}
複製程式碼

到此為止,我們的GraphQL骨架完成!雖然離實現一個有用的、功能齊全的API還需要很多步驟,但現在已經設定好了基本的核心功能。

總結和最後的想法

讓我們回顧一下本文的內容:

  • 在Node.js下可以通過Express和GraphQL庫來構建GraphQL API;
  • 基本的GraphQL使用;
  • 查詢和修改的基本用法;
  • 為專案建立模組的基本方法;
  • 測試我們的GraphQL API;

為了集中精力關注GraphQL API本身,我們忽略了幾個重要的步驟,可簡要總結如下:

  • 新專案的驗證;
  • 使用通用的錯誤服務正確處理異常;
  • 驗證使用者可以在每個請求中使用的欄位;
  • 新增JWT攔截器以保護API;
  • 使用更有效的方法處理密碼雜湊;
  • 新增單元和整合測試;

請記住,我們在Git (github.com/makinhs/nod… 並執行它!請注意,本文中提出的所有標準和建議並不是一成不變的。

這只是設計GraphQL API的眾多方法之一。此外,請務必更詳細地閱讀和探索GraphQL文件,以瞭解它提供的內容以及怎樣使你的API更好。

歡迎關注前端公眾號:前端先鋒,獲取更多前端乾貨。

用Node.js建立安全的 GraphQL API

相關文章