この GraphQL および NestJS チュートリアル (最終更新日: 2023 年 8 月) では、GraphQL API を使用するメリットについて説明します。
NestJS は、エンタープライズ グレードの効率的でスケーラブルな Node.js アプリケーションの構築に役立つ TypeScript Node.js フレームワークです。RESTful および GraphQL API 設計アプローチをサポートします。
GraphQL は API 用のクエリ言語であり、既存のデータを使用してこれらのクエリを実行するためのランタイムです。これにより、API 内のデータの完全でわかりやすい説明が提供され、顧客が必要なものを正確に尋ねることができ、時間の経過とともに API を進化させることが容易になり、強力な開発者ツールの使用が容易になります。
このチュートリアルでは、NestJS を使用して GraphQL API の機能を構築し、活用する方法を示します。以下について説明します。
記事ディレクトリ
NestJS アプリケーションを初期化する
Nest には新しいプロジェクトの生成に使用できる CLI が用意されているため、Nest プロジェクトの開始は簡単です。npm がインストールされている場合は、次のコマンドを使用して新しい Nest プロジェクトを作成できます。
npm i -g @nestjs/cli
nest new project-name
Nest は、project-name
ボイラープレート ファイルを使用してプロジェクト ディレクトリを作成し、追加します。
Nest は内部で、Nest アプリケーション内で Apollo GraphQL サーバーを使用するように構成できる 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 サーバー上のラッパーです。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 が TypeScript で利用できる最も成熟した ORM であるため、Nest で TypeORM を使用することを推奨しています。TypeScript で記述されているため、Nest フレームワークとうまく統合されます。Nest は@nestjs/typeorm
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 を構築するための 2 つのメソッド、Code-First と Schema-Firstを提供します。コードファーストのアプローチでは、TypeScript クラスとデコレーターを使用して GraphQL スキーマを生成します。このアプローチを使用すると、データ モデル クラスをスキーマとして再利用し、@ObjectType()
デコレータで修飾することができます。Nest はモデルからスキーマを自動的に生成します。一方、スキーマファーストのアプローチでは、GraphQL のスキーマ定義言語 (SDL)を使用してスキーマを定義し、スキーマ内の定義を照合してサービスを実装します。
前に述べたように、この記事ではコードファーストのアプローチを使用します。このメソッドを使用すると、@nestjs/graphql
TypeScript クラス定義のデコレーターで指定されたメタデータを読み取ることによってスキーマが生成されます。
GraphQL コンポーネント
GraphQL API は、API リクエストを実行するか、API リクエストに応答するオブジェクトを形成する複数のコンポーネントで構成されます。
リゾルバー
リゾルバーは、GraphQL 操作 (クエリ、変更、またはサブスクリプション) をデータに変換するための指示を提供します。これらは、スキーマで指定したデータ型を返すか、そのデータの Promise を返します。
この@nestjs/graphql
パッケージは、アノテーション付きクラスのデコレーターによって提供されるメタデータを使用して、リゾルバー マッピングを自動的に生成します。パッケージ機能を使用して GraphQL API を作成する方法を示すために、簡単な請求書 API を作成します。
オブジェクトの種類
オブジェクト型は、GraphQL の最も基本的なコンポーネントです。これはサービスから抽出できるフィールドのコレクションであり、各フィールドがタイプを宣言します。定義された各オブジェクト タイプは、API のドメイン オブジェクトを表し、API でクエリまたは変更できるデータの構造を指定します。たとえば、サンプルの請求書 API は顧客とその請求書のリストを取得できる必要があるため、この機能をサポートするオブジェクト タイプCustomer
を定義する必要があります。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"
この名前をデコレータに追加すると、次のようなエラーが発生した場合に役立ちます。
このエラーの別の解決策は、アプリをビルドして実行する前に出力ディレクトリを削除することです。
スキーマ
GraphQL のスキーマは、API でクエリされるデータ構造の定義です。データのフィールドとタイプ、および実行できる操作を定義します。GraphQL スキーマは、GraphQL スキーマ定義言語 (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!
}
分野
上記のクラスCustomerModel
のすべてのプロパティは、デコレータで装飾されています@Field()
。Nest では、スキーマ定義クラスで@Field()
デコレーターを明示的に使用して、各フィールドの GraphQL 型、オプション性、および null 可能性などのプロパティに関するメタデータを提供する必要があります。
フィールドの GraphQL 型は、スカラー型または他のオブジェクト型にすることができます。GraphQL には、デフォルトのスカラー型のセット ( Int
、String
ID
、 、Float
)が付属していますBoolean
。@Field()
デコレータは、オプションの型関数 (たとえば、type → Int) とオプションのオプション オブジェクトを受け入れます。
フィールドが配列の場合、デコレータの type 関数で@Field()
配列の型を手動で指定する必要があります。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 にQuery
は との 2 つの特別な型がありますMutation
。これらは他のオブジェクト タイプの親オブジェクトとして機能し、他のオブジェクトのエントリ ポイントを定義します。すべての GraphQL API には型があり、Query
Mutation
型を持つ場合と持たない場合があります。
Query
オブジェクトは、Mutation
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
。上記の例を使用して、オブジェクトが Invoice フィールドの親であることを@Resolver(of =>CustomerModel)
示し、それを Invoice フィールド リゾルバー メソッドに渡します。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 リポジトリの詳細については、こちらをご覧ください。
突然変異
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()
Modified は@Mutations()
、新しいデータを変更または追加することを示します。突然変異でパラメータとしてオブジェクトが必要な場合は、InputType
a と呼ばれる特別なタイプのオブジェクトを作成し、それをパラメータとしてメソッドに渡す必要があります。入力型を宣言するには、@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 を表示できるようになります。Playground は、グラフィカルでインタラクティブなブラウザ内 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 は柔軟性を提供します: REST に対する GraphQL の利点の 1 つは、REST リソースが提供するデータが必要よりも少ないことが多いこと (特定の機能を実現するためにユーザーが複数のリクエストを行う必要がある)、または複数のユースケースに対応するために構築する際にリソースを大量に消費することです。データが返されます。GraphQL は、各リクエストで指定されたデータ フィールドを取得して返すことで、この問題を解決します。
- GraphQL はデータを階層的に構築します: GraphQL は、データ オブジェクト間の関係をグラフのような構造で階層的に構築します。
- GraphQL は厳密に型指定されています: GraphQL はスキーマに依存します。スキーマは、各フィールドとレベルが定義された型を持つデータの厳密に型指定された定義です。
- GraphQL では、API のバージョン管理は問題になりません。API ユーザーがリクエストとレスポンスの構造を決定することを考えると、既存のユーザーを壊すことなく、API 上に構築して新しい機能やフィールドを追加することが容易になります。
注: 既存のフィールドを削除または名前変更すると、既存のユーザーに混乱が生じますが、GraphQL API を拡張する場合、REST API よりもユーザーへの混乱は少なくなります。
結論は
このチュートリアルでは、コードファーストアプローチを使用して NestJS を使用して GraphQL API を構築する方法を示します。サンプル コードの完全版は、 GitHub で共有されています。アーキテクチャファーストのアプローチとその他のベスト プラクティスの詳細については、Nest のドキュメントを参照してください。