如何使用 NestJS 构建 GraphQL API

2023-08-30 13:14:58

本 GraphQL 和 NestJS 教程最后更新于 2023 年 8 月,旨在探索使用 GraphQL API 的好处。

NestJS是一个TypeScript Node.js框架,可帮助您构建企业级,高效且可扩展的Node.js应用程序。它支持 RESTful 和 GraphQL API 设计方法。

GraphQL 是一种用于 API 的查询语言,也是使用现有数据完成这些查询的运行时。它提供了 API 中数据的完整且易于理解的描述,使客户能够准确询问他们需要的内容,使随着时间的推移更容易发展 API,并有助于使用强大的开发人员工具。

在本教程中,我们将演示如何使用 NestJS 来构建和利用 GraphQL API 的强大功能。我们将介绍以下内容:

初始化 NestJS 应用程序

启动 Nest 项目很简单,因为 Nest 提供了一个可用于生成新项目的 CLI。如果你安装了 npm,你可以使用以下命令创建一个新的 Nest 项目:

npm i -g @nestjs/cli
nest new project-name

Nest 将使用 project-name 并添加样板文件创建一个项目目录:

NestJS Boilerplate Files

在后台,Nest 公开了一个 GraphQL 模块,该模块可以配置为在 Nest 应用程序中使用 Apollo GraphQL 服务器。要将 GraphQL API 添加到我们的 Nest 项目中,我们需要安装 Apollo Server 和其他 GraphQL 依赖项:

$ npm i --save @nestjs/graphql graphql-tools graphql apollo-server-express

安装依赖项后,您现在可以导入 GraphQLModule AppModule 到 :

// src/app.module.ts

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({}),
  ],
})

export class AppModule {}

GraphQLModule 是 Apollo Server 上的包装器。它提供了一个静态方法 , forRoot() 用于配置底层 Apollo 实例。该方法 forRoot() 接受传递到 ApolloServer() 构造函数的选项列表

在本文中,我们将使用代码优先方法,该方法使用装饰器和 TypeScript 类来生成 GraphQL 模式。对于这种方法,我们需要将 autoSchemaFile 属性(创建生成的模式的路径)添加到我们的 GraphQLModule 选项中:

// src/app.module.ts

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({
       autoSchemaFile: 'schema.gql'
    }),
  ],
})

export class AppModule {}

也可以 autoSchemaFile 设置为 true ,这意味着生成的架构将保存到内存中。

Nest与数据库无关,这意味着它允许与任何数据库集成:对象文档映射器(ODM)或对象关系映射器(ORM)。出于本指南的目的,我们将使用 PostgreSQL 和 TypeORM。

Nest团队建议将TypeORM与Nest一起使用,因为它是TypeScript可用的最成熟的ORM。因为它是用TypeScript编写的,所以它与Nest框架集成得很好。Nest 提供了使用 TypeORM 的 @nestjs/typeorm 软件包。

让我们安装这些依赖项来使用 TypeORM 数据库:

$ npm install --save @nestjs/typeorm typeorm pg

安装过程完成后,我们可以使用以下命令 TypeOrmModule 连接到数据库:

// src/app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    GraphQLModule.forRoot({
      autoSchemaFile: 'schema.gql'
    }),
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'godwinekuma',
      password: '',
      database: 'invoiceapp',
      entities: ['dist/**/*.model.js'],
      synchronize: false,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule { }

构建 GraphQL API

Nest 提供了两种构建 GraphQL API 的方法:Code-First 和 Schema-First。代码优先方法涉及使用 TypeScript 类和装饰器来生成 GraphQL 模式。使用此方法,可以将数据模型类重用为架构,并使用 @ObjectType() 修饰器对其进行修饰。Nest 将从您的模型自动生成架构。同时,模式优先方法涉及使用 GraphQL 的模式定义语言 (SDL) 定义模式,然后通过匹配模式中的定义来实现服务。

如前所述,本文将使用代码优先方法。使用此方法, @nestjs/graphql 通过读取 TypeScript 类定义的装饰器中指定的元数据来生成模式。

GraphQL 组件

GraphQL API 由多个组件组成,这些组件执行 API 请求或形成其响应的对象。

Resolvers

解析器提供将 GraphQL 操作(查询、更改或订阅)转换为数据的说明。它们要么返回我们在架构中指定的数据类型,要么返回该数据的承诺。

@nestjs/graphql 包使用用于批注类的修饰器提供的元数据自动生成解析程序映射。为了演示如何使用包功能来创建 GraphQL API,我们将创建一个简单的发票 API。

Object Types

对象类型是 GraphQL 最基本的组件。它是可以从服务中提取的字段集合,每个字段声明一个类型。每个定义的对象类型表示 API 中的一个域对象,指定可在 API 中查询或更改的数据的结构。例如,我们的示例发票 API 需要能够获取客户及其发票的列表,因此我们应该定义 Customer and Invoice 对象类型以支持此功能。

对象类型用于定义 API 的查询对象、突变和架构。因为我们使用的是代码优先方法,所以我们将使用 TypeScript 类定义模式,然后使用 TypeScript 装饰器来注释这些类的字段:

// src/invoice/customer.model.ts

import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
import { ObjectType, Field } from '@nestjs/graphql';
import { InvoiceModel } from '../invoice/invoice.model';

@ObjectType()
@Entity()
export class CustomerModel {
  @Field()
  @PrimaryGeneratedColumn('uuid')
  id: string;
  @Field()
  @Column({ length: 500, nullable: false })
  name: string;
  @Field()
  @Column('text', { nullable: false })
  email: string;
  @Field()
  @Column('varchar', { length: 15 })
  phone: string;
  @Field()
  @Column('text')
  address: string;
  @Field(type => [InvoiceModel], { nullable: true })
  @OneToMany(type => InvoiceModel, invoice => invoice.customer)
  invoices: InvoiceModel[]
  @Field()
  @Column()
  @CreateDateColumn()
  created_at: Date;
  @Field()
  @Column()
  @UpdateDateColumn()
  updated_at: Date;
}

请注意,我们使用 @ObjectType() from @nestjs/graphql 装饰了类。这个装饰器告诉 NestJS 这个类是一个对象类。然后,这个 TypeScript 类将用于生成我们的 GraphQL CustomerModel 模式。

注: ObjectType 修饰器还可以选择采用正在创建的类型的名称。当遇到类似 Error: Schema must contain uniquely named types but contains multiple types named "Item" 错误时,将此名称添加到装饰器很有用。

此错误的替代解决方案是在生成和运行应用之前删除输出目录。

Schemas

GraphQL 中的模式是在 API 中查询的数据结构的定义。它定义了数据的字段、类型以及可以执行的操作。GraphQL Schemas 是用 GraphQL Schema Definition Language (SDL) 编写的。

使用代码优先方法,使用 TypeScript 类和 ObjectType 修饰器生成架构。从上面的 CustomerModel 类生成的架构将如下所示:

// schema.gql

type CustomerModel {
  id: String!
  name: String!
  email: String!
  phone: String!
  address: String!
  invoices: [InvoiceModel!]
  created_at: DateTime!
  updated_at: DateTime!
}

Field

我们 CustomerModel 上面类中的每个属性都装饰有装饰 @Field() 器。Nest 要求我们在模式定义类中显式使用 @Field() 装饰器来提供有关每个字段的 GraphQL 类型、可选性和属性的元数据,例如可为空。

字段的 GraphQL 类型可以是标量类型,也可以是其他对象类型。GraphQL 附带了一组开箱即用的默认标量类型: Int 、、 String IDFloatBoolean@Field() 装饰器接受可选的类型函数(例如,type → Int)和可选的选项对象。

当字段是数组时,我们必须在装饰器的类型函数中 @Field() 手动指示数组类型。下面是一个指示 s 数组 InvoiceModel 的示例:

 @Field(type => [InvoiceModel])
  invoices: InvoiceModel[]

现在我们已经创建了 CustomerModel 对象类型,让我们定义 InvoiceModel 对象类型:

// src/invoice/invoice.model.ts

import { CustomerModel } from './../customer/customer.model';
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, JoinColumn, ManyToOne, ChildEntity } from 'typeorm';
import { ObjectType, Field } from '@nestjs/graphql';

export enum Currency {
  NGN = "NGN",
  USD = "USD",
  GBP = "GBP",
  EUR = " EUR"
}

export enum PaymentStatus {
  PAID = "PAID",
  NOT_PAID = "NOT_PAID",
}

@ObjectType()
export class Item{
  @Field()
  description: string;
  @Field()
  rate: number;
  @Field()
  quantity: number 
}

@ObjectType()
@Entity()
export class InvoiceModel {
  @Field()
  @PrimaryGeneratedColumn('uuid')
  id: string;
  @Field()
  @Column({ length: 500, nullable: false })
  invoiceNo: string;
  @Field()
  @Column('text')
  description: string;
  @Field(type => CustomerModel)
  @ManyToOne(type => CustomerModel, customer => customer.invoices)
  customer: CustomerModel;
  @Field()
  @Column({
    type: "enum",
    enum: PaymentStatus,
    default: PaymentStatus.NOT_PAID
  })
  paymentStatus: PaymentStatus;
  @Field()
  @Column({
    type: "enum",
    enum: Currency,
    default: Currency.USD
  })
  currency: Currency;
  @Field()
  @Column()
  taxRate: number;
  @Field()
  @Column()
  issueDate: string;
  @Field()
  @Column()
  dueDate: string;
  @Field()
  @Column('text')
  note: string;
  @Field( type => [Item])
  @Column({
    type: 'jsonb',
    array: false,
    default: [],
    nullable: false,
  })
  items: Item[];
  @Column()
  @Field()
  taxAmount: number;
  @Column()
  @Field()
  subTotal: number;
  @Column()
  @Field()
  total: string;
  @Column({
    default: 0
  })
  @Field()
  amountPaid: number;
  @Column()
  @Field()
  outstandingBalance: number;
  @Field()
  @Column()
  @CreateDateColumn()
  createdAt: Date;
  @Field()
  @Column()
  @UpdateDateColumn()
  updatedAt: Date;
}

生成的 InvoiceModel 架构将如下所示:

type InvoiceModel {
  id: String!
  invoiceNo: String!
  description: String!
  customer: CustomerModel!
  paymentStatus: String!
  currency: String!
  taxRate: Float!
  issueDate: String!
  dueDate: String!
  note: String!
  Items: [Item!]!
  taxAmount: Float!
  subTotal: Float!
  total: String!
  amountPaid: Float!
  outstandingBalance: Float!
  createdAt: DateTime!
  updatedAt: DateTime!
}

GraphQL 特殊对象类型

我们已经了解了如何使用 Nest 定义对象类型。但是,GraphQL 中有两种特殊类型: QueryMutation 。它们充当其他对象类型的父对象,并定义其他对象的入口点。每个 GraphQL API 都有一个类型,可能有也可能没有 Query Mutation 类型。

QueryMutation 对象用于向 GraphQL API 发出请求。 Query 对象用于在 GraphQL API 上发出读取(即 SELECT)请求,而 Mutation 对象用于发出创建、更新和删除请求。

我们的发票 API 应该有一个 Query 返回 API 对象的对象。下面是一个示例:

type Query {
  customer: CustomerModel
  invoice: InvoiceModel
}

创建应该存在于图中的对象后,我们现在可以定义解析器类,以便为客户端提供与 API 交互的方式。

在代码优先方法中,解析程序类既定义解析程序函数又生成 Query 类型。为了创建解析器,我们将创建一个使用解析器函数作为方法的类,并使用 @Resolver() 装饰器修饰该类:

// src/customer/customer.resolver.ts

import { InvoiceModel } from './../invoice/invoice.model';
import { InvoiceService } from './../invoice/invoice.service';
import { CustomerService } from './customer.service';
import { CustomerModel } from './customer.model';
import { Resolver, Mutation, Args, Query, ResolveField, Parent } from '@nestjs/graphql';
import { Inject } from '@nestjs/common';

@Resolver(of => CustomerModel)
export class CustomerResolver {
  constructor(
    @Inject(CustomerService) private customerService: CustomerService,
    @Inject(InvoiceService) private invoiceService: InvoiceService
  ) { }
  @Query(returns => CustomerModel)
  async customer(@Args('id') id: string): Promise<CustomerModel> {
    return await this.customerService.findOne(id);
  }

  @ResolveField(returns => [InvoiceModel])
  async invoices(@Parent() customer): Promise<InvoiceModel[]> {
    const { id } = customer;
    return this.invoiceService.findByCustomer(id);
  }

  @Query(returns => [CustomerModel])
  async customers(): Promise<CustomerModel[]> {
    return await this.customerService.findAll();
  }
}

在上面的示例中,我们创建了 CustomerResolver ,它定义了一个查询解析器函数和一个字段解析器函数。为了指定该方法是查询处理程序,我们使用装饰器对 @Query() 该方法进行了注释。我们还用于 @ResolveField() 注释 invoices 解析 . CustomerModel @Args() 装饰器用于从请求中提取参数以在查询处理程序中使用。

@Resolver() 修饰器接受用于指定字段解析程序函数的父级的可选参数 of 。使用上面的示例, @Resolver(of =>CustomerModel) 指示我们的 CustomerModel 对象是字段发票的父级,并传递给发票字段解析程序方法。

上面定义的解析器类不包含从数据库中获取和返回数据所需的逻辑。相反,我们将该逻辑抽象为服务类,解析器类调用该服务类。以下是我们的客户服务类:

// src/customer/customer.service.ts

import { Injectable } from '@nestjs/common';
import { CustomerModel } from './customer.model';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CustomerDTO } from './customer.dto';
@Injectable()
export class CustomerService {
    constructor(
        @InjectRepository(CustomerModel)
        private customerRepository: Repository<CustomerModel>,
      ) {}
      create(details: CustomerDTO): Promise<CustomerModel>{
          return this.customerRepository.save(details);
      }

      findAll(): Promise<CustomerModel[]> {
        return this.customerRepository.find();
      }

      findOne(id: string): Promise<CustomerModel> {
        return this.customerRepository.findOne(id);
      }
}

TypeORM提供存储库,这些存储库连接到我们的数据实体并用于对它们执行查询。您可以在此处找到有关TypeORM存储库的更多详细信息。

Mutations

我们已经介绍了如何从 GraphQL 服务器检索数据,但是修改服务器端数据呢?正如我们前面所讨论的, Mutation 方法用于修改 GraphQL 中的服务器端数据。

从技术上讲,可以实现 来 Query 添加服务器端数据。但常见的约定是注释导致使用装饰器写入 @Mutations() 数据的任何方法。相反,装饰器告诉 Nest 这样的方法用于数据修改。

现在,让我们将新 createCustomer() 类添加到解析 CustomerResolver 器类中:

  @Mutation(returns => CustomerModel)
  async createCustomer(
    @Args('name') name: string,
    @Args('email') email: string,
    @Args('phone', { nullable: true }) phone: string,
    @Args('address', { nullable: true }) address: string,
  ): Promise<CustomerModel> {
    return await this.customerService.create({ name, email, phone, address })
  }

createCustomer() 已修饰 @Mutations() 以指示它修改或添加新数据。如果突变需要将对象作为参数,我们需要创建一种称为 InputType 特殊类型的对象,然后将其作为参数传递给方法。若要声明输入类型,请使用 @InputType() 修饰器:

import { PaymentStatus, Currency, Item } from "./invoice.model";
import { InputType, Field } from "@nestjs/graphql";

@InputType()
class ItemDTO{
    @Field()
    description: string;
    @Field()
    rate: number;
    @Field()
    quantity: number
}

@InputType()
export class CreateInvoiceDTO{
@Field()
customer: string;
@Field()    
invoiceNo: string;
@Field()
paymentStatus: PaymentStatus;
@Field()
description: string;
@Field()
currency: Currency;
@Field()
taxRate: number;
@Field()
issueDate: Date;
@Field()
dueDate: Date;
@Field()
note: string;
@Field(type => [ItemDTO])
items: Array<{ description: string; rate: number; quantity: number }>;
}


 @Mutation(returns => InvoiceModel)
  async createInvoice(
    @Args('invoice') invoice: CreateInvoiceDTO,
  ): Promise<InvoiceModel> {
    return await this.invoiceService.create(invoice)
  }

使用 GraphQL Playground 测试 GraphQL API

现在我们已经为我们的图形服务创建了一个入口点,我们可以通过操场查看我们的 GraphQL API。游乐场是一个图形化的、交互式的、浏览器内的 GraphQL IDE,默认情况下在与 GraphQL 服务器本身相同的 URL 上可用。

要访问游乐场,我们需要运行我们的 GraphQL 服务器。运行以下命令以启动服务器:

npm start

在服务器运行的情况下,打开 Web 浏览器到 http://localhost:3000/graphql 查看:

在这里插入图片描述

借助 GraphQL 操场,我们可以测试使用查询和突变对象向 API 发出请求。此外,我们可以运行如下所示 createCustomer 的 Mutation 来创建新的客户条目:

// Request
mutation {
  createCustomer(
    address: "Test Address",
    name: "Customer 1",
    email: "customer@gmail.com",
    phone: "00012344"
  ) {
    id,
    name,
    address,
    email,
    phone
  }
}

// Result
{
  "data": {
    "createCustomer": {
      "id": "0be45472-4257-4e2d-81ab-efb1221eb9f1",
      "name": "Customer 1",
      "address": "Test Address",
      "email": "customer@gmail.com",
      "phone": "00012344"
    }
  }
}

以及以下查询:

// Request
query {
  customer(id: "0be45472-4257-4e2d-81ab-efb1221eb9f1") {
    id,
    email
  }
}

// Result
{
  "data": {
    "customer": {
      "id": "0be45472-4257-4e2d-81ab-efb1221eb9f1",
      "email": "customer@gmail.com"
    }
  }
}

使用 GraphQL API 的好处

GraphQL API 因提供与服务器端 API 数据的简化和高效通信而广受欢迎。以下是构建和使用 GraphQL API 的一些好处:

  • GraphQL 请求更快:GraphQL 允许我们通过选择要查询的特定字段来减少请求和响应大小
  • GraphQL 提供了灵活性:GraphQL 相对于 REST 的优势之一是 REST 资源通常提供的数据少于所需数据(需要用户发出多个请求才能实现某些功能),或者在构建超级资源以适应多个用例的情况下返回不必要的数据。GraphQL 通过获取并返回每个请求指定的数据字段来解决此问题
  • GraphQL 分层构造数据:GraphQL 以类似图的结构分层构造数据对象之间的关系
  • GraphQL 是强类型的:GraphQL 依赖于模式,模式是数据的强类型定义,其中每个字段和级别都有定义的类型
  • 使用 GraphQL,API 版本控制不是问题:考虑到 API 用户决定其请求和响应的结构,在 API 上构建以添加新功能和字段而不会中断现有用户更容易

注意:删除或重命名现有字段仍然会对现有用户造成干扰,但当扩展 GraphQL API 时,对用户的干扰小于 REST API。

结论

在本教程中,我们演示了如何使用代码优先的方法使用 NestJS 构建 GraphQL API。您可以在 GitHub 上找到此处共享的示例代码的完整版本。要了解有关架构优先方法和其他最佳实践的更多信息,请查看 Nest 文档

更多推荐

走进JVM的内存模型

1、概述:我们在用Java语言进行编程时,并没有像C/C++程序这样为每一个new操作去写对应的delete/free操作。这得益于Java程序把内存控制权利交给JVM虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。2、JVM内存模型:JVM虚拟机在执行

基于AR增强现实模拟离心泵结构拆装与运行

通过AR模拟,学生可以虚拟地观察离心泵的结构和部件,进行拆装、安装和调试的操作,而无需实际接触物理设备。这极大地降低了学生操作过程中的风险。AR模拟离心泵的拆装过程可以分为几个步骤。首先,学生选择相应的模拟程序,然后通过平板/手机所显示的虚拟画面来观察离心泵的结构和部件。在模拟拆装过程中,学生可以用手势操作来选择需要拆

Mixin 混入

Mixin混入混入(mixin)提供了一种非常灵活的方式,来分发Vue组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。怎么理解呢,就是每一个组件都会有一些选项data、computed、methods…对吧,假设我有10个组件,每一个组件内

RabbitMQ及各种模式

目录一、MQ的基本概念1.1MQ概述1.2MQ的优势和劣势1.3MQ的优势1.应用解耦2.异步提速3.削峰填谷1.4MQ的劣势小结1.5常见的MQ产品1.6RabbitMQ简介1.7JMS小结二、RabbitMQ管控台三、HelloWorld简单模式​编辑1、生产者​编辑2、消费者​编辑四、Workqueues工作队列

CRM软件系统维护客户的主要方法

客户的重要性,相信每一个做企业的人都非常清楚。为了维护好客户,很多企业都使用CRM客户管理系统,建立“以客户为中心”的经营理念,提高企业客户服务水平,进而在提高客户满意度的同时提高企业的盈利。那么,企业如何通过CRM系统维护客户?1、客户信息管理CRM作为客户管理系统,它的主要功能就是对客户信息的收集和管理。CRM系统

超全面的前端工程化配置指南

前端工程化配置指南本文讲解如何构建一个工程化的前端库,并结合GithubActions,自动发布到Github和NPM的整个详细流程。示例我们经常看到像Vue、React这些流行的开源项目有很多配置文件,他们是干什么用的?他们的Commit、Release记录都那么规范,是否基于某种约定?废话少说,先上图!上图标红就是

使用branch and bound分支定界算法选择UTXO

BnB算法原理分支定界算法始终围绕着一颗搜索树进行的,我们将原问题看作搜索树的根节点,从这里出发,分支的含义就是将大的问题分割成小的问题。大问题可以看成是搜索树的父节点,那么从大问题分割出来的小问题就是父节点的子节点了。分支的过程就是不断给树增加子节点的过程。而定界就是在分支的过程中检查子问题的上下界,如果子问题不能产

伦敦银一手是多少?

伦敦银是以国际现货白银价格为跟踪对象的电子合约交易,无论投资者通过什么地方的平台进入市场,执行的都是统一国际的标准,一手标准的合约所代表的就是5000盎司的白银,如果以国内投资者比较熟悉的单位计算,那约相当于15.5公斤的白银。至于一手伦敦银合约的总价值是多少,具体的数值会根据国际银价的波动而变化。如果以近期每盎司25

python经典百题之求数字位数及逆序打印

题目:给一个不多于5位的正整数,要求:一、求它是几位数,二、逆序打印出各位数字程序分析我们需要编写一个程序,能够接受不多于5位的正整数,然后分析其位数,并逆序打印出各位数字。可以利用取模和除法运算来实现逆序打印数字,同时通过不断除以10的方式确定位数。方法1:使用取模和除法defreverse_print(num):p

dvwa命令执行漏洞分析

dvwa靶场命令执⾏漏洞high难度的源码:$target=trim($_REQUEST[‘ip’]);是一个接收id值的变量array_keys()函数功能是返回包含原数组中所有键名的一个新数组。str_replace()函数如下,把字符串“Helloworld!”中的字符“world”替换为“Shanghai”:s

自然语言处理(一):基于统计的方法表示单词

文章目录1.共现矩阵2.点互信息3.降维(奇异值分解)1.共现矩阵将一句话的上下文大小窗口设置为1,用向量来表示单词频数,如:将每个单词的频数向量求出,得到如下表格,即共现矩阵:我们可以用余弦相似度(cosinesimilarity)来计算单词向量的相似性:similarity⁡(x,y)=x⋅y∥x∥∥y∥=x1y1

热文推荐