使用Node構建GraphQL APIs

使用Node構建GraphQL APIs

GraphQL是API開發中的新流行語。雖然RESTful APIs仍然是暴露應用程式資料的最流行方式,但它們有許多限制,而GraphQL旨在解決這些限制。

GraphQL是由Facebook建立的一種查詢語言,在2015年變成了一個開源專案。它為描述和訪問API中的資料提供了一個直觀和靈活的語法。

本指南將探討如何建立一個GraphQL Node.js專案。我們將使用GraphQL在Node的Express.js網路框架中建立一個Todo應用程式。

  1. 什麼是GraphQL?
  2. GraphQL術語
  3. GraphQL如何與Node.js和Express.js一起工作?
  4. 用Express.js設定GraphQL

什麼是GraphQL?

來自官方文件:”GraphQL是一種用於API的查詢語言,也是用現有資料完成這些查詢的執行時間。GraphQL為你的API中的資料提供了一個完整的、可理解的描述,使客戶有能力準確地詢問他們所需要的東西,而不是更多,使API更容易隨著時間的推移而發展,並啟用強大的開發者工具。”

GraphQL是一個伺服器端的執行時間,用於使用你為你的資料定義的型別系統執行查詢。此外,GraphQL不與任何特定的資料庫或儲存引擎相聯絡。相反,它是由你現有的程式碼和資料儲存支援的。你可以通過GraphQL與RESTful API指南獲得這些技術的詳細比較。

要建立一個GraphQL服務,你首先要定義模式型別,並使用這些型別建立欄位。接下來,你提供一個函式解析器,在客戶端請求資料的時候,在每個欄位和型別上執行。

GraphQL術語

GraphQL型別系統用於描述哪些資料可以被查詢,哪些資料可以被操作。它是GraphQL的核心。讓我們討論一下我們在GraphQ中描述和運算元據的不同方式。

物件型別

GraphQL物件型別是包含強型別欄位的資料模型。在你的模型和GraphQL型別之間應該有一個1對1的對映。下面是一個GraphQL型別的例子:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type User {
id: ID! # The "!" means required
firstname: String
lastname: String
email: String
username: String
todos: [Todo] # Todo is another GraphQL type
}
type User { id: ID! # The "!" means required firstname: String lastname: String email: String username: String todos: [Todo] # Todo is another GraphQL type }
type User {
id: ID! # The "!" means required
firstname: String
lastname: String
email: String
username: String
todos: [Todo] # Todo is another GraphQL type
}

查詢

GraphQL 查詢定義了客戶端可以在 GraphQL API 上執行的所有查詢。你應該定義一個 RootQuery ,按照慣例,它將包含所有現有的查詢。

下面我們定義並將查詢對映到相應的RESTful API:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type RootQuery {
user(id: ID): User # Corresponds to GET /api/users/:id
users: [User] # Corresponds to GET /api/users
todo(id: ID!): Todo # Corresponds to GET /api/todos/:id
todos: [Todo] # Corresponds to GET /api/todos
}
type RootQuery { user(id: ID): User # Corresponds to GET /api/users/:id users: [User] # Corresponds to GET /api/users todo(id: ID!): Todo # Corresponds to GET /api/todos/:id todos: [Todo] # Corresponds to GET /api/todos }
type RootQuery {
user(id: ID): User           # Corresponds to GET /api/users/:id
users: [User]                # Corresponds to GET /api/users
todo(id: ID!): Todo    # Corresponds to GET /api/todos/:id
todos: [Todo]          # Corresponds to GET /api/todos
}

突變

如果GraphQL查詢是 GET 請求,那麼突變就是操作GraphQL API的 POSTPUTPATCH, 和 DELETE 請求。

我們將把所有的突變放在一個單一的 RootMutation 中來演示:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type RootMutation {
createUser(input: UserInput!): User # Corresponds to POST /api/users
updateUser(id: ID!, input: UserInput!): User # Corresponds to PATCH /api/users
removeUser(id: ID!): User # Corresponds to DELETE /api/users
createTodo(input: TodoInput!): Todo
updateTodo(id: ID!, input: TodoInput!): Todo
removeTodo(id: ID!): Todo
}
type RootMutation { createUser(input: UserInput!): User # Corresponds to POST /api/users updateUser(id: ID!, input: UserInput!): User # Corresponds to PATCH /api/users removeUser(id: ID!): User # Corresponds to DELETE /api/users createTodo(input: TodoInput!): Todo updateTodo(id: ID!, input: TodoInput!): Todo removeTodo(id: ID!): Todo }
type RootMutation {
createUser(input: UserInput!): User             # Corresponds to POST /api/users
updateUser(id: ID!, input: UserInput!): User    # Corresponds to PATCH /api/users
removeUser(id: ID!): User                       # Corresponds to DELETE /api/users
createTodo(input: TodoInput!): Todo
updateTodo(id: ID!, input: TodoInput!): Todo
removeTodo(id: ID!): Todo
}

你注意到在突變中使用了 -input 型別,如 UserInputTodoInput。在建立和更新你的資源時,總是定義輸入型別是最好的做法。

你可以像下面這樣定義輸入型別:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
input UserInput {
firstname: String!
lastname: String
email: String!
username: String!
}
input UserInput { firstname: String! lastname: String email: String! username: String! }
input UserInput {
firstname: String!
lastname: String
email: String!
username: String!
}

解析器

解析器告訴GraphQL在每個查詢或突變被請求時要做什麼。它是一個基本的函式,它完成了打入資料庫層進行CRUD(建立、讀取、更新、刪除)操作、打入內部RESTful API端點或呼叫微服務來完成客戶的請求的艱苦工作。

你可以建立一個新的resolvers.js檔案並新增以下程式碼:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import sequelize from '../models';
export default function resolvers () {
const models = sequelize.models;
return {
// Resolvers for Queries
RootQuery: {
user (root, { id }, context) {
return models.User.findById(id, context);
},
users (root, args, context) {
return models.User.findAll({}, context);
}
},
User: {
todos (user) {
return user.getTodos();
}
},
}
// Resolvers for Mutations
RootMutation: {
createUser (root, { input }, context) {
return models.User.create(input, context);
},
updateUser (root, { id, input }, context) {
return models.User.update(input, { ...context, where: { id } });
},
removeUser (root, { id }, context) {
return models.User.destroy(input, { ...context, where: { id } });
},
// ... Resolvers for Todos go here
}
}
import sequelize from '../models'; export default function resolvers () { const models = sequelize.models; return { // Resolvers for Queries RootQuery: { user (root, { id }, context) { return models.User.findById(id, context); }, users (root, args, context) { return models.User.findAll({}, context); } }, User: { todos (user) { return user.getTodos(); } }, } // Resolvers for Mutations RootMutation: { createUser (root, { input }, context) { return models.User.create(input, context); }, updateUser (root, { id, input }, context) { return models.User.update(input, { ...context, where: { id } }); }, removeUser (root, { id }, context) { return models.User.destroy(input, { ...context, where: { id } }); }, // ... Resolvers for Todos go here } }
import sequelize from '../models';
export default function resolvers () {
const models = sequelize.models;
return {
// Resolvers for Queries
RootQuery: {
user (root, { id }, context) {
return models.User.findById(id, context);
},
users (root, args, context) {
return models.User.findAll({}, context);
}
},
User: {
todos (user) {
return user.getTodos();
}
},
}
// Resolvers for Mutations
RootMutation: {
createUser (root, { input }, context) {
return models.User.create(input, context);    
},
updateUser (root, { id, input }, context) {
return models.User.update(input, { ...context, where: { id } });
},
removeUser (root, { id }, context) {
return models.User.destroy(input, { ...context, where: { id } });
},
// ... Resolvers for Todos go here
}
}

模式

GraphQL模式是GraphQL暴露給世界的東西。因此,型別、查詢和突變將被包含在模式中,以暴露給世界。

下面是如何將型別、查詢和突變暴露給世界的:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
schema {
query: RootQuery
mutation: RootMutation
}
schema { query: RootQuery mutation: RootMutation }
schema {
query: RootQuery
mutation: RootMutation
}

在上面的指令碼中,我們包含了我們之前建立的 RootQuery 和 RootMutation ,以暴露在世介面前。

GraphQL如何與Node.js和Express.js一起工作?

GraphQL為所有主要的程式語言提供了一個實現,Node.js也不例外。在GraphQL官方網站上,有一個支援JavaScript的部分,同時,還有GraphQL的其他實現,使編寫和編碼變得簡單。

GraphQL Apollo為Node.js和Express.js提供了一個實現,並使GraphQL容易入門。

你將在下一節中學習如何使用GraphQL Apollo在Node.js和Express.js後端框架中建立和開發你的第一個GraphQL應用程式。

用Express.js設定GraphQL

用Express.js構建GraphQL API伺服器是很簡單的,可以直接上手。在本節中,我們將探討如何建立一個GraphQL伺服器。

用Express初始化專案

首先,你需要安裝和設定一個新的Express.js專案。為你的專案建立一個資料夾,用這個命令安裝Express.js:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
cd <project-name> && npm init -y
npm install express
cd <project-name> && npm init -y npm install express
cd <project-name> && npm init -y
npm install express

上面的命令建立了一個新的package.json檔案並將Express.js庫安裝到你的專案中。

接下來,我們將按照下圖所示,構建我們的專案。它將包含專案功能的不同模組,如使用者、todos等。

graphql-todo的檔案

graphql-todo的檔案

初始化GraphQL

讓我們從安裝GraphQL Express.js的依賴項開始。執行以下命令進行安裝:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm install apollo-server-express graphql @graphql-tools/schema --save
npm install apollo-server-express graphql @graphql-tools/schema --save
npm install apollo-server-express graphql @graphql-tools/schema --save

建立模式和型別

接下來,我們要在modules資料夾內建立一個index.js檔案,並新增以下程式碼片段:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const { gql } = require('apollo-server-express');
const users = require('./users');
const todos = require('./todos');
const { GraphQLScalarType } = require('graphql');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const typeDefs = gql`
scalar Time
type Query {
getVersion: String!
}
type Mutation {
version: String!
}
`;
const timeScalar = new GraphQLScalarType({
name: 'Time',
description: 'Time custom scalar type',
serialize: (value) => value,
});
const resolvers = {
Time: timeScalar,
Query: {
getVersion: () => `v1`,
},
};
const schema = makeExecutableSchema({
typeDefs: [typeDefs, users.typeDefs, todos.typeDefs],
resolvers: [resolvers, users.resolvers, todos.resolvers],
});
module.exports = schema;
const { gql } = require('apollo-server-express'); const users = require('./users'); const todos = require('./todos'); const { GraphQLScalarType } = require('graphql'); const { makeExecutableSchema } = require('@graphql-tools/schema'); const typeDefs = gql` scalar Time type Query { getVersion: String! } type Mutation { version: String! } `; const timeScalar = new GraphQLScalarType({ name: 'Time', description: 'Time custom scalar type', serialize: (value) => value, }); const resolvers = { Time: timeScalar, Query: { getVersion: () => `v1`, }, }; const schema = makeExecutableSchema({ typeDefs: [typeDefs, users.typeDefs, todos.typeDefs], resolvers: [resolvers, users.resolvers, todos.resolvers], }); module.exports = schema;
const { gql } = require('apollo-server-express');
const users = require('./users');
const todos = require('./todos');
const { GraphQLScalarType } = require('graphql');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const typeDefs = gql`
scalar Time
type Query {
getVersion: String!
}
type Mutation {
version: String!
}
`;
const timeScalar = new GraphQLScalarType({
name: 'Time',
description: 'Time custom scalar type',
serialize: (value) => value,
});
const resolvers = {
Time: timeScalar,
Query: {
getVersion: () => `v1`,
},
};
const schema = makeExecutableSchema({
typeDefs: [typeDefs, users.typeDefs, todos.typeDefs],
resolvers: [resolvers, users.resolvers, todos.resolvers],
});
module.exports = schema;

程式碼演練

讓我們通過程式碼片斷進行工作,並將其分解:

第1步

首先,我們匯入了所需的庫,並建立了預設的查詢和變異型別。查詢和突變目前只設定了GraphQL API的版本。然而,隨著我們的進展,我們將擴充套件查詢和突變以包括其他模式。

匯入GraphQL和擴充套件

匯入GraphQL和擴充套件

第2步:

然後我們為時間建立了一個新的標量型別,併為上面建立的查詢和突變建立了我們的第一個解析器。此外,我們還使用 makeExecutableEchema 函式生成了一個模式。

生成的模式包括我們匯入的所有其他模式,當我們建立和匯入這些模式時,也將包括更多的模式。

為時間建立一個標量型別

為時間建立一個標量型別,以及我們的第一個解析器。

上面的程式碼片段顯示,我們在makeExecutableEchema函式中匯入了不同的模式。這種方法有助於我們在結構化應用程式的複雜性。接下來,我們要建立我們匯入的Todo和User模式。

建立Todo模式

Todo模式顯示了應用程式的使用者可以執行的簡單CRUD操作。下面是實現Todo CRUD操作的模式。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const { gql } = require('apollo-server-express');
const createTodo = require('./mutations/create-todo');
const updateTodo = require('./mutations/update-todo');
const removeTodo = require('./mutations/delete-todo');
const todo = require('./queries/todo');
const todos = require('./queries/todos');
const typeDefs = gql`
type Todo {
id: ID!
title: String
description: String
user: User
}
input CreateTodoInput {
title: String!
description: String
isCompleted: Boolean
}
input UpdateTodoInput {
title: String
description: String
isCompleted: Boolean
} extend type Query {
todo(id: ID): Todo!
todos: [Todo!]
}
extend type Mutation {
createTodo(input: CreateTodoInput!): Todo
updateTodo(id: ID!, input: UpdateTodoInput!): Todo
removeTodo(id: ID!): Todo
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
// Resolvers for Queries
Query: {
todo,
todos,
},
// Resolvers for Mutations
Mutation: {
createTodo,
updateTodo,
removeTodo,
},
};
module.exports = { typeDefs, resolvers };
const { gql } = require('apollo-server-express'); const createTodo = require('./mutations/create-todo'); const updateTodo = require('./mutations/update-todo'); const removeTodo = require('./mutations/delete-todo'); const todo = require('./queries/todo'); const todos = require('./queries/todos'); const typeDefs = gql` type Todo { id: ID! title: String description: String user: User } input CreateTodoInput { title: String! description: String isCompleted: Boolean } input UpdateTodoInput { title: String description: String isCompleted: Boolean } extend type Query { todo(id: ID): Todo! todos: [Todo!] } extend type Mutation { createTodo(input: CreateTodoInput!): Todo updateTodo(id: ID!, input: UpdateTodoInput!): Todo removeTodo(id: ID!): Todo } `; // Provide resolver functions for your schema fields const resolvers = { // Resolvers for Queries Query: { todo, todos, }, // Resolvers for Mutations Mutation: { createTodo, updateTodo, removeTodo, }, }; module.exports = { typeDefs, resolvers };
const { gql } = require('apollo-server-express');
const createTodo = require('./mutations/create-todo');
const updateTodo = require('./mutations/update-todo');
const removeTodo = require('./mutations/delete-todo');
const todo = require('./queries/todo');
const todos = require('./queries/todos');
const typeDefs = gql`
type Todo {
id: ID!
title: String
description: String
user: User
}
input CreateTodoInput {
title: String!
description: String
isCompleted: Boolean
}
input UpdateTodoInput {
title: String
description: String
isCompleted: Boolean
}  extend type Query {
todo(id: ID): Todo!
todos: [Todo!]
}
extend type Mutation {
createTodo(input: CreateTodoInput!): Todo
updateTodo(id: ID!, input: UpdateTodoInput!): Todo
removeTodo(id: ID!): Todo
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
// Resolvers for Queries
Query: {
todo,
todos,
},
// Resolvers for Mutations
Mutation: {
createTodo,
updateTodo,
removeTodo,
},
};
module.exports = { typeDefs, resolvers };

程式碼演練

讓我們通過程式碼片斷進行工作,並將其分解:

第1步:

首先,我們使用GraphQL typeinput, 和 extend 為我們的Todo建立一個模式。extend 關鍵字是用來繼承和新增新的查詢和突變到我們上面建立的現有根查詢和突變。

為Todo建立模式

為Todo建立模式

第2步:

接下來,我們建立了一個解析器,用於在呼叫特定查詢或變異時檢索正確的資料。

建立解析器

建立解析器

使用解析器函式,我們可以為業務邏輯和資料庫操作建立單獨的方法,如create-todo.js示例所示。

./mutations 資料夾中建立一個create-user.js檔案,並新增業務邏輯以在資料庫中建立一個新的Todo。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const models = require('../../../models');
module.exports = async (root, { input }, context) => {
return models.todos.push({ ...input });
};
const models = require('../../../models'); module.exports = async (root, { input }, context) => { return models.todos.push({ ...input }); };
const models = require('../../../models');
module.exports = async (root, { input }, context) => {
return models.todos.push({ ...input });
};

上面的程式碼片段是使用Sequelize ORM在我們的資料庫中建立一個新Todo的簡化方法。你可以瞭解更多關於Sequelize以及如何用Node.js設定它

你可以按照同樣的步驟,根據你的應用程式建立許多模式,或者你可以從GitHub上克隆整個專案

接下來,我們將用Express.js設定伺服器,並使用GraphQL和Node.js執行新建立的Todo應用程式。

設定和執行伺服器

最後,我們將使用之前安裝的 apollo-server-express 庫設定我們的伺服器,並對其進行配置。

apollo-server-express 是一個用於Express.js的Apollo伺服器的簡單包裝,它被推薦是因為它已經被開發為適合Express.js的開發。

使用我們上面討論的例子,讓我們配置Express.js伺服器,使其與新安裝的 apollo-server-express一起工作。

在根目錄下建立一個server.js檔案並貼上以下程式碼:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const schema = require('./modules');
const app = express();
async function startServer() {
const server = new ApolloServer({ schema });
await server.start();
server.applyMiddleware({ app });
}
startServer();
app.listen({ port: 3000 }, () =>
console.log(`Server ready at http://localhost:3000`)
);
const express = require('express'); const { ApolloServer } = require('apollo-server-express'); const schema = require('./modules'); const app = express(); async function startServer() { const server = new ApolloServer({ schema }); await server.start(); server.applyMiddleware({ app }); } startServer(); app.listen({ port: 3000 }, () => console.log(`Server ready at http://localhost:3000`) );
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const schema = require('./modules');
const app = express();
async function startServer() {
const server = new ApolloServer({ schema });
await server.start();
server.applyMiddleware({ app });
}
startServer();
app.listen({ port: 3000 }, () =>
console.log(`Server ready at http://localhost:3000`)
);

在上面的程式碼中,你已經為Todos和Users成功建立了你的第一個CRUD GraphQL伺服器。你可以啟動你的開發伺服器並使用http://localhost:3000/graphql 來訪問遊樂場。如果一切成功,你應該看到下面的螢幕:

驗證螢幕

驗證螢幕

小結

GraphQL是由Facebook支援的現代技術,它簡化了使用RESTful架構模式建立大規模API的繁瑣工作。

本指南闡明瞭GraphQL,並演示瞭如何用Express.js開發你的第一個GraphQL API。

請在下面的評論中告訴我們你用GraphQL建立了什麼。

評論留言