Node.js和GraphQl教程:如何使用Apollo服务器构建GraphQl API
#网络开发人员 #node #api #graphql

简介
欢迎使用node.js使用Apollo服务器构建GraphQl API的教程!在本指南中,我将向您展示如何利用GraphQl的功能为您的应用程序创建高效且灵活的API。

那么,GraphQl到底是什么?想象一个世界,您可以准确地从服务器中索取所需的数据,而不再和更少。 GraphQl是一种查询语言,可以使您这样做。与传统的REST API不同,您经常在预定义的端点中收到固定的数据集,而GraphQl可以使您构成查询以符合您的特定要求。这就像在您的触手可及。

将GraphQl与REST进行比较,就像将定制的西装与现成的衣服进行比较。休息后,您可能最终会过度提取或取得不足的数据,从而导致效率低下。但是使用GraphQL,您可以自由要求仅提供所需的字段,消除不必要的数据传输并优化应用程序的性能。

但这不是全部! GraphQl还具有将多个数据源合并到单个查询中的能力。在不同端点之间不再有杂耍来组装所需的数据。 GraphQl在一个优雅的请求中将其全部融合在一起。

无论您是构建一个简单的待办应用程序还是复杂的电子商务平台,GraphQl的灵活性和效率都可以彻底改变您与API的互动方式。在整个教程中,我将逐步指导您使用Node.js的Apollo服务器创建GraphQl API。您将学习如何使用解析器来定义数据模式和从MongoDB获取数据。

所以,如果您准备潜入GraphQl的世界并解锁其潜力,那么让我们开始!

文件夹结构
当潜入使用Apollo服务器构建GraphQl API时,具有清晰且有条理的文件夹结构是关键。这是针对您项目的文件夹结构的建议布局,使与传统的Restful API项目中的事情相似。

project-root/
|-- src/
|   |-- schema/           # GraphQL schema definitions
|   |-- resolvers/        # Resolver functions for handling queries and mutations
|   |-- models/           # Data models or database schemas
|   |-- app.js            # GraphQL server setup
|-- package.json          # Project dependencies and scripts
|-- .gitignore            # Git ignore configurations
|-- .env                  # Environment variables (optional)

这是每个文件夹代表的:

  • 模式:将“架构”文件夹视为与静止API中的路由或端点相似。在这里,您可以使用架构定义语言(SDL)定义GraphQl架构。该模式概述了您的API支持的类型,查询,突变甚至订阅。这是API结构的核心。

  • 解析器:就像控制器处理RESTFUL API中不同路由的逻辑一样,“解析器”文件夹中的解析器处理了GraphQl架构中各个字段的逻辑。每个解析器函数对应于一个特定字段,并包含获取数据,与数据库交互并执行所需操作的实际代码。解析器是“魔术”发生的地方,类似于控制器在RESTFUL API中执行操作的方式。

  • 模型:“模型”文件夹包含您的数据模型,这些模型等同于静止API上下文中的数据库架构。这些模型定义了数据的结构和关系。在解析器中,您将这些模型与您的数据源进行交互,就像在Restful API的数据库层中一样。

  • app.js:“ app.js”文件扮演设置和配置GraphQl Server的角色。这等同于您可能在RESTFUL API的主文件中进行的服务器配置和中间件设置。

  • 软件包。它列出了您项目管理API的依赖项和脚本,就像在一个Restful API项目中一样。

  • .gitignore:类似于一个RESTFUL API项目,“ .gitignore”文件可帮助您指定应该被版本控制(GIT)忽略的文件和目录。

  • .env:可选,您可以使用“ .env”文件来存储应用程序的环境变量,就像在Restful API中一样。

请记住,虽然该建议的文件夹结构与Restful Design相似,但它适合您项目的特定需求和开发偏好。它提供了一个有效地组织GraphQL API项目的路线图,利用您可能已经熟悉的概念来使用Restful API。

软件包
对于此项目,您需要安装以下软件包:
mongoosebcryptjsdotenv@apollo/server @graphql-tools/merge

让我们简要地谈论这些包:

  1. mongoose :MongoDB和Node.js的ODM库,帮助数据建模和交互。
  2. bcryptjs :用于安全密码哈希和比较的库。
  3. dotenv :从.env文件加载环境变量,确保安全配置。
  4. @apollo/server :用于流线型架构执行和验证的GraphQL Server实现。
  5. @graphql-tools/merge :将多个GraphQl模式组合到一个内聚模式中的实用程序。

因为我正在使用Typescript,我的文件夹结构和包装。JSON文件现在看起来像这样。

folder structure

现在使用下面的代码更新./src/db/connect.ts文件,以建立与您的Mongo数据库的连接。

import mongoose from "mongoose";

export const connectDB = (url : string) => {
    return mongoose.connect(url)
    .then(() => console.log("Connected to database"))
    .catch((err) => console.log(err));
}

对于此项目,我创建了两个模型-UserProducts。这就是他们的样子:

./src/model/user.ts

import {Schema, Model, model, Document} from 'mongoose';
import bcrypt from 'bcryptjs';

export interface IUser extends Document {
    username: string;
    email: string;
    password: string;
    isValidPassword: (password: string) => Promise<boolean>;
}

const UserSchema: Schema = new Schema({
    username: {type: String, required: true, unique: true},
    email: {type: String, required: true},
    password: { type: String, required: true}
})

UserSchema.pre('save', async function() {
    const salt = await bcrypt.genSalt(10);
    this.password = await bcrypt.hash(this.password, salt);
})

UserSchema.methods.isValidPassword = async function(password: string) {
    const compare = await bcrypt.compare(password, this.password);
    return compare;
}

export const User: Model<IUser> = model<IUser>('User', UserSchema);

./src/model/product.ts

import {Schema, Model, model, Document} from 'mongoose'

export interface IProduct extends Document {
    name: string;
    price: number;
}

const ProductSchema: Schema = new Schema({
    name: {type: String, required: true},
    price: {type: Number, required: true}
})

export const Product: Model<IProduct> = model<IProduct>('Product', ProductSchema);

现在,我们已经成功地为我们的API创建了一个数据库架构,让我们直接向我们的GraphQl查询和突变创建模式。

对于用户架构,更新./src/schema/user.ts看起来像这样:

import { buildSchema } from "graphql";

export const usersGQLSchema = buildSchema(`
    type User {
        id: String!
        username: String!
        email: String!
        password: String!
    }

    type Query {
        users: usersInfoResponse!
        user(id: String!): User!
    }

    type usersInfoResponse {
        success: Boolean!
        total: Int!
        users: [User!]!
    }

    type Mutation {
        regUser(username: String!, email: String!, password: String!): User!
        loginUser(email: String!, password: String!): User!
        updateUser(id: String!, username: String, email: String, password: String): User!
        deleteUser(id: String!): deleteResponse!
    }

    type deleteResponse {
        success: Boolean!
        message: String!
        id: String!
    }

`)

让我分解该代码呈现的所有内容:

1。类型 - 像数据结构
Type User:就像在Restful API中一样,GraphQl中的“类型”定义了数据的样子。例如,“用户”就像用户数据的蓝图,包括ID,用户名,电子邮件和密码等属性。

2。查询 - 检索数据
Query users:将其视为请求用户列表的一种方式。类似于RESTFUL API端点,您正在询问用户信息。 usersInfoResponse之后的感叹点(!)表明此查询始终返回用户信息的响应。

Query user(id):这就像获取有关一个用户的详细信息,就像您通过提供ID中的RESTFUL API一样。 ID参数标记有一个感叹号(!),以表明查询工作是必需的。用户之后的感叹点表示此查询始终返回用户信息。

3。突变 - 修改数据
Mutation regUser/loginUser:类似于在REST中创建新资源的类似,此突变使您可以注册/签署新用户。用户名,电子邮件和密码之后的感叹号表明创建用户需要这些字段。用户之后的感叹点表示该突变始终返回新创建的用户的信息。

Mutation updateUser(id):这就像更新用户的信息一样,可与REST中的资源进行编辑。需要ID,您可以修改用户名,电子邮件或密码。如果您不提供感叹号,则意味着该字段是可选的。

Mutation deleteUser(id):就像您可能删除REST中的资源一样,此突变会删除用户。 ID是必需的,并且deleterSponse之后的感叹号表明它始终返回响应。

4。自定义响应类型 - 结构化响应
Type usersInfoResponse:这就像您在请求休息中的用户列表时可能会得到的响应。成功之后的感叹点,总计和用户意味着这些字段始终包含在响应中。

Type deleteResponse:在删除REST中删除资源时,这种类型始终包含成功,消息和ID。

本质上,GraphQl的感叹点突出了所需字段,就像在Restful API中一样。它们确保当您进行查询或突变时,您可以肯定地收回所需的数据。

创建用户模式后,让我们创建解析器(或控制器)以实现逻辑。

使用此代码更新./src/resolver/user.ts

import {User} from '../model/user';

interface Args {
    id: string;
    username: string;
    email: string;
    password: string;
}

export const UsersResolver = {
    Query : {
        users: async () => {
            try {
                const users = await User.find({});
                if (!users) throw new Error('No users found');
                return {
                    success: true,
                    total: users.length,
                    users
                };
            } catch (error) {
                throw error;
            }
        },    

        user: async (_ : any, args : Args) => {
            try {
                if (!args.id) throw new Error('No id provided');
                const user = await User.findById(args.id);
                if (!user) throw new Error('No user found');
                return user;
            } catch (error) {
                throw error;
            }
        }
    },

    Mutation : {
        regUser: async (_ : any, args : Args) => {
            try {
                const user = await User.findOne({email: args.email});
                if (user) throw new Error('User already exists');
                const newUser = await User.create({
                    username: args.username,
                    email: args.email,
                    password: args.password
                })
                return newUser;
            } catch (error) {
                throw error;
            }
        },

        loginUser: async (_ : any, args : Args) => {
            try {
                const user = await User.findOne({email: args.email});
                if (!user) throw new Error('User not found');
                const isValid = await user.isValidPassword(args.password);
                if (!isValid) throw new Error('Invalid password');
                return user;
            } catch (error) {
                throw error;
            }
        },

        updateUser: async (_ : any, args : Args) => {
            try {
                const id = args.id;
                if (!id) throw new Error('No id provided');
                const user = await User.findById(args.id);
                if (!user) throw new Error('User not found');
                const updateUser = await User.findByIdAndUpdate(id, {...args}, {new: true, runValidators: true});
                return updateUser;
            } catch (error) {
                throw error;
            }
        },

        deleteUser: async (_ : any, args : Args) => {
            try {
                const id = args.id;
                if (!id) throw new Error('No id provided');
                const user = await User.findById(args.id);
                if (!user) throw new Error('User not found');
                const deleteUser = await User.findByIdAndDelete(id);
                return {
                    success: true,
                    message: 'User deleted successfully',
                    id: deleteUser?._id
                };
            } catch (error) {
                throw error;
            }
        }
    }
}

在上面的代码中:

创建了UsersResolver对象,其中包含用于查询和突变的解析器。

查询部分包含两个解析器:

users:从数据库中获取所有用户,并返回包含有关用户信息的响应。
user:通过数据库提供的ID获取用户。
突变部分包含几个用于各种操作的解析器,每个都执行特定动作:

regUser:如果新用户在数据库中还不存在。
loginUser:验证用户凭据并在登录成功的情况下返回用户。
updateUser:根据其ID更新用户的信息。
deleteUser:通过其ID删除用户。
每个解析器都使用异步代码(异步/等待)与数据库进行交互并处理潜在错误。

args接口定义了解析器函数的预期参数。例如,每个突变都需要ID,用户名,电子邮件和密码参数。

此代码演示了GraphQL解析器如何从外部模块与用户模型进行交互时,以获取,创建,更新和删除数据。它类似于您可能在控制器中用于静止API的逻辑,每个解析器都对应于特定的API操作。

伟大的工作,既然我们已经成功地实现了Users的逻辑,让我们通过为Products创建模式和解析器来重复相同的过程。

./src/schema/products.ts

import {buildSchema} from "graphql"

export const productsGQLSchema = buildSchema(`
    type Product {
        id: String!
        name: String!
        price: Int!
    }

    type Query {
        products: productsInfoResponse!
        product(id: String!): Product!
    }

    type productsInfoResponse {
        success: Boolean!
        total: Int!
        products: [Product!]!
    }

    type Mutation {
        addProduct(name: String!, price: Int!): Product!
        updateProduct(id: String!, name: String, price: Int): Product!
        deleteProduct(id: String!): deleteResponse!
    }

    type deleteResponse {
        success: Boolean!
        message: String!
        id: String!
    }
`)

./src/resolvers/products.ts

import { Product } from "../model/products";

interface Args {
    id: string;
    name: string;
    price: number;
}

export const ProductsResolver = {
    Query : {
        products: async () => {
            try {
                const products = await Product.find({});
                if (!products) throw new Error('No products found');
                return {
                    success: true,
                    total: products.length,
                    products
                };
            } catch (error) {
                throw error;
            }
        },

        product: async (_ : any, args : Args) => {
            try {
                if (!args.id) throw new Error('No id provided');
                const product = await Product.findById(args.id);
                if (!product) throw new Error('No product found');
                return product;
            } catch (error) {
                throw error;
            }
        }
    },

    Mutation : {
        addProduct: async (_ : any, args : Args) => {
            try {
                const product = await Product.findOne({name: args.name});
                if (product) throw new Error('Product already exists');
                const newProduct = await Product.create({
                    name: args.name,
                    price: args.price
                })
                return newProduct;
            } catch (error) {
                throw error;
            }
        },

        updateProduct: async (_ : any, args : Args) => {
            try {
                const id = args.id;
                if (!id) throw new Error('No id provided');
                const product = await Product.findById(args.id);
                if (!product) throw new Error('No product found');
                const updateProduct = await Product.findByIdAndUpdate(id, {...args}, {new: true, runValidators : true});
                return updateProduct;
            } catch (error) {
                console.log(error)
            }
        },

        deleteProduct: async (_ : any, args : Args) => {
            try {
                const id = args.id;
                if (!id) throw new Error('No id provided');
                const product = await Product.findById(args.id);
                if (!product) throw new Error('No product found');
                const deleteProduct = await Product.findByIdAndDelete(id);
                return {
                    success: true,
                    message: 'Product deleted successfully',
                    id: deleteProduct?._id
                };
            } catch (error) {
                throw error;
            }
        }
    }
}

类似于User实体的实施,Product实体的模式和解析器无缝协作,为管理与产品相关的操作提供全面且结构化的方法。这种有凝聚力的互动可确保在GraphQL API中进行查询,创建,更新和删除产品,从而有效地和逻辑地处理。

产品架构用作蓝图,定义了Product类型的结构。就像User类型一样,此模式概述了构成产品的字段,例如ID,名称,描述,价格等。它不仅指定字段本身,而且指定其数据类型以及它们是否需要或可选。

另一方面,产品解析器负责功能方面。它类似于用户解析器,它封装了涉及产品的查询和突变背后的实际逻辑。例如,在查询产品列表时,解析器从数据源(例如,数据库。在我们的情况下 - mongodb)获取产品,并构建了一个良好的响应,并提供有关每种产品的详细信息。同样,在创建,更新或删除产品时,解析器处理必要的数据操作,验证以及与数据源的互动。

在串联中,架构和解析器形成了一个凝聚力的单元,使您可以直接理解,实现和维护GraphQL API中与产品相关的操作。定义结构(架构)与实施功能(解析)之间的关注点分开,有助于清洁和有组织的代码库,使您的API开发体验更加顺畅,更结构化。

现在是时候将所有架构和解析器组合起来了,以便我们可以将合并的类型定义和模式导入到app.ts文件中。

更新./src/schema/index.ts

import {mergeTypeDefs} from "@graphql-tools/merge"

import { usersGQLSchema } from "./user"
import { productsGQLSchema } from "./products"

export const mergedGQLSchema = mergeTypeDefs([usersGQLSchema, productsGQLSchema])

这样做,您正在创建一个统一的GraphQL模式,该模式包含了用户和产品模式中定义的所有类型和操作。然后可以将此合并的模式导入到您的app.ts文件中,以创建支持用户和与产品相关功能的凝聚力GraphQl API。

更新./src/resolver/index.ts

import { UsersResolver } from "./user";
import { ProductsResolver } from "./product";

export const resolvers = [UsersResolver, ProductsResolver]

当我们设置您的GraphQl Server时,您将使用此组合的解析器和合并的模式来创建功能齐全的GraphQL API。

我们即将完成,最后一步是构建我们的app.ts文件,我们将合并到目前为止开发的各种组件。该文件将用作我们的GraphQl应用程序的骨干。

开始,我们使用dotenv库从.env文件加载环境变量,以确保敏感数据(例如数据库连接详细信息和端口号)的安全配置。

进口基本依赖性遵循。其中包括负责连接到数据库(connectDB)的功能,以及促进GraphQl Server创建的ApolloServerstartStandaloneServer模块。

在此上下文中,我们定义一个名为PORT的常数,以封装服务器的端口号。此值要么从环境变量中提取,要么是默认为3000

我们的ApolloServer实例是中心舞台。我们使用合并的GraphQL架构(mergedGQLSchema)和组合解析器(resolvers)配置它。此外,我们启用内省,这是使用GraphQl Playground等工具进行内省模式的宝贵工具。

为了使一切栩栩如生,start功能出现了。作为异步功能,它精心策划了设置过程:

  • 它使用connectDB函数来建立与MongoDB数据库的连接,使用来自环境变量的URI。
  • 然后调用startStandaloneServer功能,启动Apollo服务器的操作。该服务器在指定的端口(PORT)上专心听。
  • 此序列的结晶标志着宣布服务器成功启动的控制台消息。

完成这些步骤后,我们通过调用start函数来达到该过程。此操作点燃了旅程,连接数据库并将GraphQl Server推向操作。

这就是我们的app.ts文件的样子:

require("dotenv").config()

import { connectDB } from "./db/connect";

import { ApolloServer } from '@apollo/server';

import { startStandaloneServer } from '@apollo/server/standalone';

import { mergedGQLSchema } from "./schema";
import { resolvers } from "./resolvers";

const PORT = parseInt(process.env.PORT as string) || 3000

const server = new ApolloServer({
    typeDefs : mergedGQLSchema,
    resolvers : resolvers,
    introspection : true
  });

const start = async () => {
    try {
        connectDB(process.env.MONGO_URI as string)
        startStandaloneServer(server, { listen: { port: PORT } });
        console.log(`Server is listening on port ${PORT}`)
    } catch (error) {
        console.log(error)
    }
}

start()

这样,我们都设定了。要将服务器设置在运动中,只需执行命令npm run devnpm start即可。之后,查明http://localhost:3000的终点。如果组件按预期降低到位,则您的浏览器将将您运送到Interactive Apollo GraphQl Playground。

这是一个出色的功能,因为GraphQl本质上是自我记录的。 GraphQl Playground是一种集成的查询工具,可让您轻松测试和构建查询。就像在邮递员这样的工具中找到的功能一样,您可以制定查询,探索模式并获得对API的功能的见解。

看起来像这样:

Apollo graphql playground

查询获取所有用户:

get all users

注册用户的突变:

register user

更新产品的突变

Update product

请注意,所有端点均可通过http://localhost:3000/访问,这与您需要为每条路线定义不同的端点不同的设计不同。这是因为GraphQl的单端点体系结构及其以更具动态的方式处理复杂数据检索的能力。

在传统的Restful API设计中,每个端点通常对应于特定的资源或路线。如果您想检索不同类型的数据,则需要为每个资源创建不同的端点。例如,要获取用户信息,您可能具有像GET /users这样的端点,对于产品,诸如GET /products的另一个端点。

但是,GraphQl采用了不同的方法。使用GraphQL,有一个单个端点是所有数据操作的入口点。通常通过HTTP POST请求访问此端点。 GraphQl不是为不同资源定义多个端点,而是采用灵活的查询系统,使您可以准确要求所需的数据,仅此而已。

这是GraphQl查询语言的功能。客户端可以通过创建与GraphQL架构中定义的类型和字段相匹配的查询来指定所需数据的形状和结构。这就像要求根据您的应用程序需求量身定制的数据响应。

幕后,GraphQl Server处理查询并仅检索请求的数据。这消除了为不同用例创建和管理众多端点的需求。单端点方法简化了API结构,减少了冗余,并提供了一种更有效的与数据交互的方法。

本质上,GraphQl的单端点设计以及其动态查询功能提供了一种更简化和适应性的方法来处理复杂数据检索,与传统的Restful API的更坚固的端点结构相比。这有助于GraphQL为现代API开发带来的效率和灵活性。

结论
总之,GraphQl提出了许多优势,使其成为现代API开发的引人注目的选择。它的灵活查询系统使客户能够准确地要求他们需要的数据,从而消除了与Restful API相关的数据过度提取和不足的数据。数据传输中的这种优化可增强性能,降低不必要的网络流量并导致更快,更有效的交互。

与经常需要多个端点的RESTFUL API不同,GraphQL的单端点体系结构简化了API管理并减少了版本控制的需求。使用GraphQl,您可以自由地发展API而不会引起现有客户的干扰,因为可以添加或弃用字段而无需更改端点结构。

此外,GraphQl的内省功能授予开发人员访问深入的模式文档,使其成为自我文献的API。这与集成查询工具(例如GraphQl Playground,简化开发,调试和测试)相结合。

但是,必须确认GraphQl可能不是每种情况的最佳解决方案。它的灵活性可能会导致复杂的查询,并有可能在服务器上承受较重的负载。另一方面,由于其可预测的性质,静止的API可以为基础数据模型和缓存管理提供更清晰的映射。

相比,GraphQl和Restful API都具有其优点和劣势,可满足不同的项目要求。尽管GraphQl在优先级的灵活性,有效的数据检索和统一端点的情况下表现出色,但静止的API可以更适合于清晰,面向资源的结构和缓存机制至关重要的情况。

>

>

在构建GraphQl API的旅程中,我们遍历了创建模式,定义类型,制作解析器和设置服务器的过程。本教程的完整代码可通过GRAPHQL-API在GitHub上访问,为您的努力提供了实用的参考。

对于所有开始进行此探索的读者,我对加入本教程表示感谢。无论您选择GraphQl还是Restful API,您的编码旅程都可以充满创新,效率和变革性的体验。愉快的编码!