如何使用 NestJS 构建 GraphQL API

本 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: "[email protected]",
    phone: "00012344"
  ) {
    
    
    id,
    name,
    address,
    email,
    phone
  }
}

// Result
{
    
    
  "data": {
    
    
    "createCustomer": {
    
    
      "id": "0be45472-4257-4e2d-81ab-efb1221eb9f1",
      "name": "Customer 1",
      "address": "Test Address",
      "email": "[email protected]",
      "phone": "00012344"
    }
  }
}

以及以下查询:

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

// Result
{
    
    
  "data": {
    
    
    "customer": {
    
    
      "id": "0be45472-4257-4e2d-81ab-efb1221eb9f1",
      "email": "[email protected]"
    }
  }
}

使用 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 文档

猜你喜欢

转载自blog.csdn.net/jslygwx/article/details/132580265