用Node.js創(chuàng)建安全的 GraphQL API

翻譯:瘋狂的技術(shù)宅
https://www.toptal.com/graphql/graphql-nodejs-api

本文的目標是提供關(guān)于如何創(chuàng)建安全的 Node.js GraphQL API 的快速指南丰捷。

你可能會想到一些問題:

  • 使用 GraphQL API 的目的是什么?
  • 什么是GraphQL API寂汇?
  • 什么是GraphQL查詢棘脐?
  • GraphQL的好處是什么昏滴?
  • GraphQL是否優(yōu)于REST?
  • 為什么我們使用Node.js?

這些問題都是有意義的煞抬,但在回答之前低飒,我們應(yīng)該深入了解當前 Web 開發(fā)的狀態(tài):

  • 現(xiàn)在幾乎所有的解決方案都使用了某種應(yīng)用程序編程接口(API)霹崎。
  • 即使你只用社交網(wǎng)絡(luò)(如Facebook或Instagram)胯陋,仍然會用到使用API??的前端。
  • 如果你感到好奇扒磁,你會發(fā)現(xiàn)幾乎所有在線娛樂服務(wù)都在用不同類型的API庆揪,包括Netflix,Spotify和YouTube等妨托。

你會發(fā)現(xiàn)幾乎在每種情況下都會有一個不需要你去詳細了解的API缸榛,例如你不需要知道它們是怎樣構(gòu)建的检访,并且不需要使用與他們相同的技術(shù)就能夠?qū)⑵浼傻侥阕约旱南到y(tǒng)中。API允許你提供一種可以在服務(wù)器和客戶端通信之間進行通用標準通信的方式仔掸,而不必依賴于特定的技術(shù)棧脆贵。

通過結(jié)構(gòu)良好的API,可以擁有可靠起暮、可維護且可擴展的API卖氨,可以為多種客戶端和前端應(yīng)用提供服務(wù)。

什么是 GraphQL API负懦?

GraphQL 是一種 API 所使用的查詢語言筒捺,由Facebook開發(fā)并用于其內(nèi)部項目,并于2015年公開發(fā)布纸厉。它支持讀取系吭、寫入和實時更新等操作。同時它也是開源的颗品,通常會與REST和其他架構(gòu)放在一起進行比較肯尺。簡而言之,它基于:

  • GraphQL查詢 —— 允許客戶端進行讀取和控制接收數(shù)據(jù)的方式躯枢。
  • GraphQL 修改 —— 描述怎樣在服務(wù)器上寫入數(shù)據(jù)则吟。關(guān)于怎樣將數(shù)據(jù)寫入系統(tǒng)的GraphQL約定。

雖然本文應(yīng)該展示一個關(guān)于如何構(gòu)建和使用GraphQL API的簡單但真實的場景锄蹂,但我們不會去詳細介紹GraphQL氓仲。因為GraphQL團隊提供了全面的文檔,并在Introduction to GraphQL中列出了幾個最佳實踐得糜。

什么是GraphQL查詢敬扛?

如上所述,查詢是客戶端從API讀取和操作數(shù)據(jù)的一種方式朝抖。你可以傳遞對象的類型啥箭,并選擇要接收的字段類型。下面是一個簡單的查詢:

query{
  users{
    firstName,
    lastName
  }
}

我們嘗試從用戶庫中查詢所有用戶槽棍,但只接收firstNamelastName捉蚤。此查詢的結(jié)果將類似于:

{
  "data": {
    "users": [
      {
        "firstName": "Marcos",
        "lastName": "Silva"
      },
      {
        "firstName": "Paulo",
        "lastName": "Silva"
      }
    ]
  }
}

客戶端的使用非常簡單抬驴。

使用GraphQL API的目的是什么炼七?

創(chuàng)建API的目的是使自己的軟件具有可以被其他外部服務(wù)集成的能力。即使你的程序被單個前端程序所使用布持,也可以將此前端視為外部服務(wù)豌拙,為此,當通過API為兩者之間提供通信時题暖,你能夠在不同的項目中工作按傅。

如果你在一個大型團隊中工作捉超,可以將其拆分為創(chuàng)建前端和后端團隊,從而允許他們使用相同的技術(shù)唯绍,并使他們的工作更輕松拼岳。

在本文中,我們將重點介紹怎樣構(gòu)建使用GraphQL API的框架况芒。

GraphQL比REST更好嗎惜纸?

GraphQL是一種適合多種情況的方法。 REST是一種體系結(jié)構(gòu)方法绝骚。如今耐版,有大量的文章可以解釋為什么一個比另一個好,或者為什么你應(yīng)該只使用REST而不是GraphQL压汪。另外你可以通過多種方式在內(nèi)部使用GraphQL粪牲,并將API的端點維護為基于REST的架構(gòu)。

你應(yīng)該做的是了解每種方法的好處止剖,分析自己正在創(chuàng)建的解決方案腺阳,評估你的團隊使用解決方案的舒適程度,并評估你是否能夠指導你的團隊快速掌握這些技術(shù)穿香。

本文更偏重于實用指南舌狗,而不是GraphQL和REST的主觀比較。如果你想查看這兩者的詳細比較扔水,我建議你查看我們的另一篇文章痛侍,為什么GraphQL是API的未來

在今天的文章中魔市,我們將專注于怎樣用Node.js創(chuàng)建GraphQL API主届。

為什么要使用Node.js?

GraphQL有好幾個不同的支持庫可供使用待德。出于本文的目的君丁,我們決定使用Node.js環(huán)境下的庫,因為它的應(yīng)用非常廣泛将宪,并且Node.js允許開發(fā)人員使用他們熟悉的前端語法進行服務(wù)器端開發(fā)绘闷。

掌握GraphQL

我們將為自己的 GraphQL API 設(shè)計一個構(gòu)思的框架,在開始之前较坛,你需要了解Node.js和Express的基礎(chǔ)知識印蔗。這個GraphQL示例項目的源代碼可以在這里找到(https://github.com/makinhs/node-graphql-tutorial)。

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

  • Users 丑勤,處理基本的CRUD华嘹。
  • Products, 我們對它的介紹會詳細一點法竞,以展示GraphQL更多的功能耙厚。

Users 包含以下字段:

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

Products 包含以下字段:

  • id
  • name
  • description
  • price

至于編碼標準强挫,我們將在這個項目中使用TypeScript。

讓我們開始編碼薛躬!

首先俯渤,要確保安裝了最新的Node.js版本。在本文發(fā)布時型宝,在Nodejs.org上當前版本為10.15.3稠诲。

初始化項目

讓我們創(chuàng)建一個名為node-graphql的新文件夾,并在終端或Git CLI控制臺下使用以下命令:npm init诡曙。

配置依賴項和TypeScript

為了節(jié)約時間臀叙,在我們的Git存儲庫中找到以下代碼去替換你的package.json應(yīng)該包含的依賴項:

{
  "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后,在終端中執(zhí)行:npm install价卤。

接著是配置我們的TypeScript模式劝萤。在根文件夾中創(chuàng)建一個名為tsconfig.json的文件,其中包含以下內(nèi)容:

{
  "compilerOptions": {
    "target": "ES2016",
    "module": "commonjs",
    "outDir": "./build",
    "strict": true,
    "esModuleInterop": true
  }
}

這個配置的代碼邏輯將會出現(xiàn)在app文件夾中慎璧。在那里我們可以創(chuàng)建一個app.ts文件床嫌,在里面添加以下代碼用于基本測試:

console.log('Hello Graphql Node API tutorial');

通過前面的配置,現(xiàn)在我們可以運行 npm start 進行構(gòu)建和測試了胸私。在終端控制臺中厌处,你應(yīng)該能夠看到輸出的字符串“Hello Graphql Node API tutorial”。在后臺場景中岁疼,我們的配置會將 TypeScript 代碼編譯為純 JavaScript阔涉,然后在build文件夾中執(zhí)行構(gòu)建。

現(xiàn)在為GraphQL API配置一個基本框架捷绒。為了開始我們的項目瑰排,將添加三個基本的導入:

  • Express
  • Express-graphql
  • Graphql-tools

把它們放在一起:

import express from 'express';
import graphqlHTTP from 'express-graphql';
import {makeExecutableSchema} from 'graphql-tools';

現(xiàn)在應(yīng)該能夠開始編碼了。下一步是在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服務(wù)器啟用端口3000椭住。
  • 定義我們想要用作快速示例的查詢和修改。
  • 定義查詢和修改的工作方式字逗。

好的京郑,但是typeDefs和resolvers中發(fā)生了什么,它們與查詢和修改的關(guān)系又是怎樣的呢葫掉?

  • typeDefs - 我們可以從查詢和修改中獲得的模式的定義些举。
  • Resolvers - 在這里我們定義了查詢和修改的功能和行為,而不是想要的字段或參數(shù)挖息。
  • Queries - 我們想要從服務(wù)器讀取的“獲取方式”金拒。
  • Mutations - 我們的請求將會影響在自己的服務(wù)器上的數(shù)據(jù)兽肤。

現(xiàn)在讓我們再次運行npm start套腹,看看我們能得到些什么绪抛。我們希望該程序運行后產(chǎn)生這種效果:Graphql API 偵聽3000端口。

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

服務(wù)器測試

好了电禀,現(xiàn)在可以編寫第一個自己的查詢了幢码,先定義為“hello”。

第一次查詢

請注意尖飞,我們在typeDefs中定義它的方式症副,頁面可以幫助我們構(gòu)建查詢。

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

現(xiàn)在沮明,讓我們看看當我們用mutation對值進行改變時會發(fā)生什么:

mutation 演示

現(xiàn)在我們可以用GraphQL Node.js API進行基本的CRUD操作了辕坝。接下來開始使用這些代碼。

Products

對于Products荐健,我們將使用名為products的模塊酱畅。為了是本文不那么啰嗦,我們將用內(nèi)存數(shù)據(jù)庫進行演示江场。先定義一個模型和服務(wù)來管理Products纺酸。

我們的模型將基于以下內(nèi)容:

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通信的服務(wù)定義為:

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模塊相同的結(jié)構(gòu)址否。我們將為用戶提供模型和服務(wù)餐蔬。該模型將定義為:

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

}

同時,我們的服務(wù)將會是這樣:

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

    }

}

提醒一下佑附,源代碼可以在 https://github.com/makinhs/node-graphql-tutorial 找到用含。

現(xiàn)在運行并測試我們的代碼。運行npm start帮匾,將在端口3000上運行服務(wù)器啄骇。我們現(xiàn)在可以通過訪問http://localhost:3000/graphql來測試自己的GraphQL

嘗試一個mutation,將一個項目添加到我們的product列表中:

GraphQL mutation 演示

為了測試它是否有效瘟斜,我們現(xiàn)在使用查詢缸夹,但只接收idnameprice

query{
  products{
    id,
    name,
    price
  }
}

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

很好螺句,按照預期工作了∷洳眩現(xiàn)在可以根據(jù)需要獲取字段了。你可以試著添加一些描述:

query{
  products{
    id,
    name,
    description,
    price
  }
}

現(xiàn)在我們可以對product進行描述蛇尚。接下來試試user吧芽唇。

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

查詢?nèi)缦拢?/p>

query{
  users{
    id,
    firstName,
    lastName,
    password,
    email
  }
}

返回內(nèi)容如下:

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

到此為止,我們的GraphQL骨架完成!雖然離實現(xiàn)一個有用的匆笤、功能齊全的API還需要很多步驟研侣,但現(xiàn)在已經(jīng)設(shè)置好了基本的核心功能。

總結(jié)和最后的想法

讓我們回顧一下本文的內(nèi)容:

  • 在Node.js下可以通過Express和GraphQL庫來構(gòu)建GraphQL API;
  • 基本的GraphQL使用;
  • 查詢和修改的基本用法;
  • 為項目創(chuàng)建模塊的基本方法;
  • 測試我們的GraphQL API;

為了集中精力關(guān)注GraphQL API本身炮捧,我們忽略了幾個重要的步驟庶诡,可簡要總結(jié)如下:

  • 新項目的驗證;
  • 使用通用的錯誤服務(wù)正確處理異常;
  • 驗證用戶可以在每個請求中使用的字段;
  • 添加JWT攔截器以保護API;
  • 使用更有效的方法處理密碼哈希;
  • 添加單元和集成測試;

請記住,我們在Git (https://github.com/makinhs/node-graphql-tutorial)上有完整的源代碼咆课∧┦模可以隨意使用、fork书蚪、提問喇澡、pull 并運行它!請注意殊校,本文中提出的所有標準和建議并不是一成不變的撩幽。

這只是設(shè)計GraphQL API的眾多方法之一。此外箩艺,請務(wù)必更詳細地閱讀和探索GraphQL文檔窜醉,以了解它提供的內(nèi)容以及怎樣使你的API更好。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末艺谆,一起剝皮案震驚了整個濱河市榨惰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌静汤,老刑警劉巖琅催,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異虫给,居然都是意外死亡藤抡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門抹估,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缠黍,“玉大人,你說我怎么就攤上這事药蜻〈墒剑” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵语泽,是天一觀的道長贸典。 經(jīng)常有香客問我,道長踱卵,這世上最難降的妖魔是什么廊驼? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上妒挎,老公的妹妹穿的比我還像新娘绳锅。我一直安慰自己,他們只是感情好饥漫,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布榨呆。 她就那樣靜靜地躺著罗标,像睡著了一般庸队。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上闯割,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天彻消,我揣著相機與錄音,去河邊找鬼宙拉。 笑死宾尚,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的谢澈。 我是一名探鬼主播煌贴,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼锥忿!你這毒婦竟也來了牛郑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤敬鬓,失蹤者是張志新(化名)和其女友劉穎淹朋,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钉答,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡础芍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了数尿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仑性。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖右蹦,靈堂內(nèi)的尸體忽然破棺而出虏缸,到底是詐尸還是另有隱情,我是刑警寧澤嫩实,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布刽辙,位于F島的核電站,受9級特大地震影響甲献,放射性物質(zhì)發(fā)生泄漏宰缤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望慨灭。 院中可真熱鬧朦乏,春花似錦、人聲如沸氧骤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽筹陵。三九已至刽锤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間朦佩,已是汗流浹背并思。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留语稠,地道東北人宋彼。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像仙畦,于是被迫代替她去往敵國和親输涕。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容