JavaScript >> Javascript 文檔 >  >> Node.js

在 Node.js 中創建 GraphQL 服務器

毫無疑問,您聽說過 GraphQL,這是 Facebook 的基於圖形的查詢語言。自 2015 年發布以來,越來越多的數據提供商一直在提供 GraphQL 端點。此端點通常與傳統的基於 REST 的 API 一起提供。

我開始更喜歡前端的 GraphQL 端點。我喜歡能夠查詢我想要的特定數據,避免過度或不足的問題。我喜歡 GraphQL 的自我記錄特性,因為它基於類型的模式準確地描述了預期和返回的內容。我與 REST API 搏鬥了太多次,才意識到文檔已經過時或錯誤。

不過,在後端,我繼續提供 REST 端點。傳統的 HTTP 動詞和路由很熟悉,我可以很快得到一些功能性的東西。

我想在本文中回答的問題是,如何啟動並運行 GraphQL API?

上下文

為了幫助給本文提供一些背景信息,我創建了一個虛構的衝浪店。今年夏天我經常去皮划艇,這就是這家商店的賣點。本文附帶的代碼可以在這裡找到。

我的衝浪店使用 MongoDB 數據庫,並且有一個 Fastify 服務器準備就緒。如果您想跟隨,您可以在此處找到該商店的啟動代碼以及播種腳本。您將需要安裝 Node 和 MongoDB,這超出了本文的範圍,但點擊名稱進入安裝頁面。

為了讓這成為一個現實的場景,我希望在我添加 GraphQL 端點時讓當前使用 REST API 的客戶不受影響。

讓我們開始吧!

GraphQL 架構

我們需要將兩個庫添加到我們的項目中以啟動和運行 GraphQL。不出所料,第一個是 graphql ,第二個是mercurius . Mercurius 是 GraphQL 的 Fastify 適配器。讓我們安裝它們:

yarn add graphql mercurius

GraphQL 是基於模式的,這意味著我們的 API 將始終記錄在案並且類型安全。這對我們的消費者來說是一個顯著的好處,有助於我們思考數據之間的關係。

我們的商店有兩種類型,CraftOwner .導航到 Mongoose 模型,您可以查看每個模型上可用的字段。我們來看看Owner 型號。

Mongoose 模型如下所示:

const ownerSchema = new mongoose.Schema({
  firstName: String,
  lastName: String,
  email: String,
});

我們將創建一個模式目錄,它是一個 index.js 文件,然後創建我們的 GraphQL 模式。這個OwnerType 在這個架構中看起來與 Mongoose 非常相似。

const OwnerType = `type OwnerType {
  id: ID!
  firstName: String
  lastName: String
  email: String
}`;

模板字符串用於定義我們的類型,以關鍵字 type 開頭 以及我們類型的名稱。與 JavaScript 對像不同,類型定義的每一行後面都沒有逗號。相反,每一行都有字段名稱及其類型,用冒號分隔。我用過 IDString 在我的定義中輸入。您會注意到 ID 後面有一個感嘆號 ! ,它將其標記為強制的、不可為空的字段。所有其他字段都是可選的。

我要將此類型添加到 Query 現在我的架構類型。

const schema = `
type Query {
  Owners: [OwnerType]
  Owner(id: ID!): OwnerType
}

${OwnerType}
`;

你會看到 Owners 被鍵入為返回 OwnerType 的數組 , 用方括號表示。

Owner 要求查詢消費者傳遞一個 id 字段。這由括號中的值表示,(id: ID!) ,同時顯示字段的名稱和必須確認的類型。

最後,我們將從這個文件中導出這個模式並將其導入我們的主 index.js 文件。

module.exports = { schema };

const { schema } = require("./schema");

當我們導入 schema 時,我們可以導入 mercurius 插件並註冊到 Fastify。

const mercurius = require("mercurius");

fastify.register(mercurius, {
  schema,
  graphiql: true,
});

在選項插件中,我們將傳遞架構和另一個屬性 - 我們將設置 graphiql 等於真。

GraphiQL

GraphiQL 是一個基於瀏覽器的界面,旨在探索和使用您的 GraphQL 端點。現在它設置為 true,我們可以運行我們的服務器並導航到 http://localhost:3000/graphiql 找到這個頁面。

有了這個工具,我們可以做到以下幾點:

  1. 編寫並驗證我們的查詢。
  2. 添加查詢變量和請求標頭以幫助進行測試。
  3. 從我們的 API 獲取結果。
  4. 探索我們的架構生成的文檔。

探索模式現在顯示 query: Query 的根類型 .我們為這種類型添加了 OwnerOwners .點擊這裡顯示如下:

並單擊其中任何一個都會顯示相應的類型:

我將繼續設置其餘的類型定義。您可以查看源代碼以了解我是如何添加 Craft 輸入並添加一個 crafts Owner 的字段 輸入。

完成後,我的 Query 類型現在如下所示:

字段關係已全部建立,但我們還無法從中獲取任何數據。為此,我們需要探索兩個概念:查詢和解析器。

GraphQL 查詢

GraphQL 的核心是一種查詢語言。它甚至在名稱中!但是,到目前為止,我們還沒有執行任何查詢。 GraphiQL 工具具有自動完成功能,因此我們現在可以開始構建查詢。以下查詢應返回所有 Crafts 的名稱 .

query {
  Crafts {
    name
  }
}

但是,當我們執行時,我們得到一個 null 回應。

{
  "data": {
    "Crafts": null
  }
}

那是因為我們沒有設置任何解析器。解析器是 GraphQL 運行以查找解析查詢所需的數據的函數。

對於這個項目,我將在 schema/index.js 中定義解析器 文件,在模式旁邊。我已經為我的 REST API 路由使用的兩種數據類型提供了控制器。我將使用這些控制器,經過一些調整,為我的 GraphQL 端點提供服務。

首先,我將導入控制器:

const craftController = require("../controllers/craftController");
const ownerController = require("../controllers/ownerController");

然後,我將創建一個解析器對象:

const resolvers = {}

對於我們要為其提供解析器的每個根類型,該對象應該有一個鍵。對於我們的使用,我們有一個單一的根類型,即 Query .此鍵的值應該是執行以獲取所需數據的函數。這就是我們的 Crafts 字段的外觀:

const resolvers = {
  Query: {
    async Crafts() {
      return await craftController.getCrafts();
    },
  },
};

然後我們導出解析器函數,將其導入我們的主 index.js ,並將其與架構一起傳遞給我們的插件選項對象。

// in /src/schema/index.js
module.exports = { schema, resolvers };

// in /src/index.js
const { schema, resolvers } = require("./schema");

fastify.register(mercurius, {
  schema,
  resolvers,
  graphiql: true,
});


現在,當我們運行前面的查詢時,我們應該得到數據庫中所有工藝品的名稱。

驚人的!但是,如果我們要查詢特定的工藝怎麼辦?這需要更多的工作。首先,讓我們在 GraphiQL 編輯器中構造查詢。

查詢設置看起來非常相似,但有一些不同:

  1. 我需要傳入一個查詢變量。在關鍵字 query 之後 ,我們指明要傳遞的變量的名稱和類型。變量應以美元符號 ($ )。
  2. 這裡,我使用的是變量$id 作為要在我的 Craft 字段上查詢的字段的值。
  3. 查詢變量的值作為 JSON 傳遞。
  4. 最後,我收到了回复。

目前,我沒有返回任何數據。讓我們解決這個問題!

回到我的解析器中,我將為 Craft 添加一個函數。第一個位置參數是父級,此操作不需要它,因此我將在此處使用下劃線。第二個是傳入查詢的參數,我想從中分解 id:

const resolvers = {
  Query: {
    async Crafts() {
      return await craftController.getCrafts();
    },
    async Craft(_, { id }) {
      return await craftController.getCraftById({id})
    },
  },
};

目前,我的 getCraftById 函數期待請求對象。我需要更新 src/controllers/craftController.js 中的函數 .

這個原函數

// Get craft by id
exports.getCraftById = async (request, reply) => {
  try {
    const craft = await Craft.findById(request.params.id);
    return craft;
  } catch (error) {
    throw boom.boomify(error);
  }
};

變成了

exports.getCraftById = async (request, reply) => {
  try {
    const id = request.params === undefined ? request.id : request.params.id;
    const craft = await Craft.findById(id);
    return craft;
  } catch (error) {
    throw boom.boomify(error);
  }
};

驚人的!現在,當我們執行查詢時,將返回一個結果。

我們需要幫助 GraphQL 填充鏈接到其他類型的字段。如果我們的消費者查詢該工藝的當前所有者,它將返回為 null .我們可以根據owner_id添加一些邏輯來獲取所有者 , 存儲在數據庫中。然後可以將其附加到我們的工藝對像上,然後再傳回給我們的用戶。

async Craft(_, { id }) {
  const craft = await craftController.getCraftById({ id });
  if (craft && craft.owner_id) {
    const owner = await ownerController.getOwnerById({
      id: craft.owner_id,
    });
    craft.owner = owner;
  }
  return craft;
},

我們的 ownerController.getOwnerById 將需要以與相應工藝功能相同的方式進行更新。但是,一旦處理好了,我們就可以自由地查詢所有者了。

您可以檢查完成的代碼目錄以找到所有其他字段的解析器和更新的控制器功能。

GraphQL 突變

我現在可以自信地提供對 GraphQL 端點的查詢;所有讀取操作都是對我們已經完成的操作的一些改編。其他操作呢?具體來說,Create , Update , 和 Delete ?

在 GraphQL 中,這些操作中的每一個都稱為突變。我們正在以某種方式更改數據。為突變設置後端幾乎與設置查詢完全相同。我們需要在schema中定義mutation,然後提供調用mutation時會執行的resolver函數。

所以,在 /schema/index.js , 我要擴展 Mutation 輸入並添加 addCraft 突變。

type Mutation {
  addCraft(  
    name: String
    type: String
    brand: String
    price: String
    age: Int
  ): CraftType
}

與之前的字段定義一樣,括號中的值顯示了可以將哪些字段傳遞給函數。這每個都與它們的類型一起傳遞。然後我們關注突變將返回的內容。在這種情況下,我們的 CraftType 形狀的對象。

在 GraphiQL 中檢查這一點,我們可以看到 mutation 現在是一個根類型,當我們點擊時,我們的 addCraft 突變存在於模式中。

在 GraphiQL 中構建突變看起來與構建查詢相同。我們需要像以前一樣傳入查詢變量,它看起來像這樣:

但是,當我們執行時,我們得到一個 null 回复。希望這並不令人驚訝,因為我們還沒有為這種突變創建解析器。現在就開始吧!

我們將添加一個 Mutation 解析器對象的關鍵和我們的 addCraft 的函數 突變。

Mutation: {
  async addCraft(_, fields) {
    const { _id: id } = await craftController.addCraft({ ...fields });
    const craft = { id, ...fields };
    return craft;
  },
},

我們當前的 addCraft 函數只返回 Mongoose 響應,即 _id 場地。我們將提取它並返回輸入的字段,使我們能夠符合我們之前聲明的 CraftType。

更新和銷毀功能的配置和設置是相同的。在每種情況下,我們都在模式中擴展 Mutation 類型並添加相應的解析器。

您可以檢查完成的代碼目錄以找到其他一些突變的解析器。

結論

我開始懷疑構建 GraphQL 服務器是否是一個巨大的不必要的麻煩。我非常有信心在我的下一個後端項目中使用 GraphQL。

與通過我們的 REST API 直接接觸 Mongo 相比,最初的設置和样板要多一些。這可能是一個癥結所在。不過,我認為有一些令人信服的觀點值得這樣做。

您不再需要為應用程序的某些特定用途提供端點。消費者只需要調用給定上下文所需的字段。這樣可以節省雜亂的路由文件和對 API 的多次調用。

在更新模式和解析器時,您可以立即向您的消費者提供這些數據。雖然您可以將字段標記為已棄用,但您可以將遺留字段保留在適當的位置,而用戶的成本很低。此外,這是一個自記錄 API。您的文檔站點再也不會與您的 API 的當前狀態不同步了。

你確信嗎?你會轉向 GraphQL,還是永遠使用團隊 REST API?


Tutorial JavaScript 教程
  1. 大 O 分析很棒,除非它不是

  2. React 代碼重構

  3. 💡 原生檢測環境光的變化。

  4. 使用 Flask 和 React 構建全棧 Twitter 克隆 |第1部分

  5. JavaScript 錯誤處理的最完整指南

  6. 使用 Node.js 和 Express.js 的簡易 HTTP/2 服務器

  7. 嘗試 Node.js

  1. Reactjs 與 Laravel 或 Lumen 或 Express

  2. 基本內容渲染

  3. 深入了解 Vue 3 - 可組合的可重用性

  4. Appwrites 帳戶服務

  5. 將元素旋轉到固定軸

  6. 免費的 Vue.js Bulma 管理儀表板 2020

  7. 學習編碼的最佳學習技術

  1. 使用 Hooks 在 React 中構建一個 CRUD 應用程序

  2. 我的所有文章合併

  3. AWS SNS 入門

  4. 如何在您的 Web 應用程序中輕鬆創建炫酷的進度條?